bzoj 2824: [AHOI2012]铁盘整理

来源:互联网 发布:淘宝皮草品牌 编辑:程序博客网 时间:2024/04/30 05:51

题目描述

输入输出格式

输入格式:

共两行。第一行为铁盘个数N(1<=N<=50),第二行为N个不同的正整数,分别为从上到下的铁盘的半径R。(1<=R<=100)

输出格式:

一个正整数,表示使铁盘从小到大有序需要的最少翻转次数。

输入输出样例

输入样例#1:
52 4 3 5 1
输出样例#1:
5
此题乍一看数据范围极小,自然以为是水题,便想用bfs直接水过去,结果很满意的得了10分,以下是暴力程序:

#include<cmath>#include<cstdio>#include<cstdlib>#include<cstring>#include<iostream>#include<algorithm>#include<queue>#define inf 999999999#define For(i,a,b) for(i=a;i<=b;++i)#define rep(i,a,b) for(i=a;i>=b;--i)#define mm(a,b) memset(a,b,sizeof(a))#define ll long longusing namespace std;int read(){    int sum=0,flag=1;    char c=getchar();    while(c<'0'||c>'9'){if(c=='-')flag=-1;c=getchar();}    while(c>='0'&&c<='9')sum=sum*10+c-'0',c=getchar();    return sum*flag;}int maxx(int x,int y){    if(x<y)return y;    return x;}int minn(int x,int y){    if(x<y)return x;    return y;}int abss(int x){    if(x>=0)return x;    return -x;}const int maxn = 100010;int a[51],b[51];struct node{    int s[51],t;};int n,cnt;node tmp;void bfs(){    int i,j;    node T;    memcpy(T.s,a,sizeof(a));    T.t=0;    queue<node>q;    q.push(T);    while(!q.empty()){        T=q.front();        q.pop();        int jicun=n;        while(T.s[jicun]==b[jicun])jicun--;        For(i,2,jicun){            memcpy(tmp.s,T.s,sizeof(T.s));            int k=i>>1;            For(j,1,k){//直接翻转                int ch=tmp.s[j];                tmp.s[j]=tmp.s[i-j+1];                tmp.s[i-j+1]=ch;            }                tmp.t=T.t+1;                if(memcmp(tmp.s,b,sizeof(b))==0){//如果到达目标状态则结束                    printf("%d\n",T.t+1);                    return ;                }                q.push(tmp);        }    }}int main(){    int i,j;    n=read();    For(i,1,n){        a[i]=read();        b[i]=a[i];    }    sort(b+1,b+n+1);    if(memcmp(a,b,sizeof(b))==0){        printf("1\n");        return 0;    }    bfs();    return 0;}
我发现是状态太多的原因,于是想用trie判重,但是数组小一点便re,大一点就mle,还是十分,仅贴一个程序作为纪念:

#include<cmath>#include<cstdio>#include<cstdlib>#include<cstring>#include<iostream>#include<algorithm>#include<queue>#define inf 999999999#define For(i,a,b) for(i=a;i<=b;++i)#define rep(i,a,b) for(i=a;i>=b;--i)#define mm(a,b) memset(a,b,sizeof(a))#define ll long longusing namespace std;int read(){    int sum=0,flag=1;    char c=getchar();    while(c<'0'||c>'9'){if(c=='-')flag=-1;c=getchar();}    while(c>='0'&&c<='9')sum=sum*10+c-'0',c=getchar();    return sum*flag;}int maxx(int x,int y){    if(x<y)return y;    return x;}int minn(int x,int y){    if(x<y)return x;    return y;}int abss(int x){    if(x>=0)return x;    return -x;}const int maxn = 100010;int a[51],b[51];struct node{    int s[51],t;};int n,cnt;int trie[2000000][51];node tmp;int pd(){//字典树部分    int i,u=0,flag=0;    For(i,1,n){        if(!trie[u][tmp.s[i]]){            flag=1;            trie[u][tmp.s[i]]=++cnt;        }        u=trie[u][tmp.s[i]];    }    if(flag)return 1;    return 0;}void bfs(){    int i,j;    node T;    memcpy(T.s,a,sizeof(a));    T.t=0;    queue<node>q;    q.push(T);    while(!q.empty()){        T=q.front();        q.pop();        For(i,2,n){            memcpy(tmp.s,T.s,sizeof(T.s));            int k=i>>1;            For(j,1,k){                int ch=tmp.s[j];                tmp.s[j]=tmp.s[i-j+1];                tmp.s[i-j+1]=ch;            }            if(pd()){//判重                tmp.t=T.t+1;                if(memcmp(tmp.s,b,sizeof(b))==0){                    printf("%d\n",T.t+1);                    return ;                }                q.push(tmp);            }        }    }}int main(){    int i,j;    n=read();    For(i,1,n){        a[i]=read();        b[i]=a[i];    }    sort(b+1,b+n+1);    if(memcmp(a,b,sizeof(b))==0){        printf("1\n");        return 0;    }    bfs();    return 0;}
我开始认真对待这道题,一些题解里说最多可以只翻2*n次但都没有解释清楚,但身为蒟蒻的我一时半会儿想不通,以下就写出证明过程以供其他蒟蒻参考:

每一次确定最末尾的那个数,步骤是第一步,把最末尾未确定的数换到第一个位置,接下来就可以换到最后了,接下来一样的步骤,举个例子:

对于 2 4 3 5 1,最开始要确定最后一位,也就是要把最后一位变成5,于是先把5放到第一位,->5 3 4 2 1,再换到最后1 2 4 3 5,之后5就不管他了

只剩下1 2 4 3,于是一样的->4 2 1 3->3 1 2 4,还有3个数,3 1 2->2 1 3->1 2 3于是就排好了,算下来,最坏情况只需要2*n-1步

于是就有了第一个剪枝(大于2*n-1的情况可以直接减掉),然而有了这个剪枝还是没有什么用,我们还要利用另外一个原理:若两个数在目前位置上相邻,但在目标位置上不相邻,则因为是翻转,因此至少需要一步把这两个数分开,因此从当前排列到目标排列最少就需要出现这种情况的总和。待会代码里还会解释:

#include<cmath>#include<cstdio>#include<cstdlib>#include<cstring>#include<iostream>#include<algorithm>#include<queue>#define inf 999999999#define For(i,a,b) for(i=a;i<=b;++i)#define rep(i,a,b) for(i=a;i>=b;--i)#define mm(a,b) memset(a,b,sizeof(a))#define ll long longusing namespace std;int read(){    int sum=0,flag=1;    char c=getchar();    while(c<'0'||c>'9'){if(c=='-')flag=-1;c=getchar();}    while(c>='0'&&c<='9')sum=sum*10+c-'0',c=getchar();    return sum*flag;}int maxx(int x,int y){    if(x<y)return y;    return x;}int minn(int x,int y){    if(x<y)return x;    return y;}int abss(int x){    if(x>=0)return x;    return -x;}const int maxn = 100010;struct node{int value,pos,nowpos;};node a[maxn];bool cmp(node c,node d){return c.value<d.value;}int ans,n;void dfs(int sum,int total){//sum是已走过的步数,total是当前排列到达目标状态的预计步数if(!total&&a[1].nowpos==1){ans=minn(ans,sum);return;}//若目标步数为0,且第一个位置上是对的,这是为了预防如5 4 3 2 1这种情况的出现int i,j;For(i,2,n){//枚举翻前i位int zong=total;if(i<n){//如果i位置与i+1位置相邻且预计位置不相同,则可以通过这步操作分开他们,同时预计步数减少,但要考虑换过来后第1个数和第i+1个数的位置关系zong-=(abss(a[i].nowpos-a[i+1].nowpos)!=1)-(abss(a[1].nowpos-a[i+1].nowpos)!=1);}if(zong+sum<ans){//若当前步数加上预计步数大于已搜到的,就剪掉int mid=i/2;For(j,1,mid){int tmp=a[j].nowpos;a[j].nowpos=a[i-j+1].nowpos;a[i-j+1].nowpos=tmp;}dfs(sum+1,zong);For(j,1,mid){//回溯int tmp=a[j].nowpos;a[j].nowpos=a[i-j+1].nowpos;a[i-j+1].nowpos=tmp;}}}}int main(){int i,j;n=read();For(i,1,n){a[i].value=read();a[i].pos=i;}sort(a+1,a+n+1,cmp);For(i,1,n){a[a[i].pos].nowpos=i;//把当前位置的目标位置记录}int sum=0;ans=n*2-1;//至多搜n*2-1次For(i,2,n){sum+=abss(a[i].nowpos-a[i-1].nowpos)!=1;//如果当前排列中位置相邻但目标排列中并不相邻则预计步数加1}dfs(0,sum);printf("%d\n",ans);return 0;} 
剪枝万岁!

2 0