• [SDOI 2015] 排序(深搜+阶乘)(考试)


    题干:
      小A有一个1~2N的排列A[1..2N],他希望将数组A从小到大排序。小A可以执行的操作有N种,每种操作最多可以执行一次。对于所有的i(1<=i<=N),第i种操作为:将序列从左到右划分成2N-i+1段,每段恰好包含2i-1个数,然后整体交换其中的两段。小A想知道可以将数组A从小到大排序的不同的操作序列有多少个。小A认为两个操作序列不同,当且仅当操作的个数不同,或者至少一个操作不同(种类不同或者操作的位置不同)。

    下面是一个操作示例:

    N=3,初始排列A[1..8]为[3,6,1,2,7,8,5,4]。

    第一次操作:执行第3种操作,交换A[1..4]和A[5..8],交换后的A[1..8]为[7,8,5,4,3,6,1,2];

    第二次操作:执行第1种操作,交换A[3]和A[5],      交换后的A[1..8]为[7,8,3,4,5,6,1,2];

    第三次操作:执行第2种操作,交换A[1..2]和A[7..8],交换后的A[1..8]为[1,2,3,4,5,6,7,8]。

    对于30%的数据,1<=N<=4;对于全部的数据,1<=N<=12。

    题解:

      首先,通过题干所给出的例子,我们应该很容易即可得出:在一种可实现的情况中,操作的顺序并不会影响最终结果,所以结果一定是与阶乘有关(也就是在每一种交换成功的总操作中,它的阶乘就是他所能作出的贡献)。题干中又说每次只能移动两块且每个操作只能用一次,不难想到我们应该将一个大串分成无数个小串来处理(且若在一个操作之中出现太多不匹配的情况,一定不会对最终结果作出贡献)。

    30%TLE:

      在考场上只想到30%的算法。先看一下数据范围,n在1~4时其实dfs很轻松就能跑过,无非是枚举每一种操作,要么交换,要么不交换,跑一遍暴搜即可。(枚举时从小操作到大操作或从大操作到小操作都可,毕竟是暴搜。。。)在考场上手模了几个点,发现都只是一个阶乘(有点蒟蒻。。。其实应该是几种操作数阶乘的和),就用最优化剪枝找到交换次数最少的结果,输出了它的阶乘(还用了exit(0);。。。),撞大运拿下6个点(下面的代码大家慎点。。。)。

    Code:

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<algorithm>
     4 #define $ 5000
     5 using namespace std;
     6 int a[$],m,n,k,t,aa[$],b[$],dp[22][$],ans=100000000;
     7 inline int A(int x,int ans=1){
     8     if(x>n) return 0;
     9     for(register int i=2;i<=x;++i) ans*=i;
    10     return ans;
    11 }
    12 inline void dfs(int x,int sum){
    13     if(sum>ans) return;
    14     for(register int i=1;i<=m;++i){
    15         if(a[i]!=i) break;
    16         if(i==m){    ans=min(ans,sum); return;    }
    17     }
    18     if(x==1){
    19         int ci=0;
    20         for(register int i=1;i<=m;++i){
    21             if(a[i]!=i) ci++;
    22             if(ci>2) break;
    23         }
    24         if(ci<=2) ans=min(ans,sum+1);
    25         return;
    26     }
    27     int ch=1<<(x-1);
    28     for(register int i=1;i<=m;i+=ch){
    29         for(register int j=1;j<=m;j+=ch){
    30             if(i==j) continue;
    31             for(register int k=0;k<ch;++k) swap(a[i+k],a[j+k]);
    32             dfs(x-1,sum+1);
    33             for(register int k=0;k<ch;++k) swap(a[i+k],a[j+k]);
    34             dfs(x-1,sum);
    35         }
    36     }
    37 }
    38 signed main(){
    39     scanf("%d",&n);
    40     m=1<<n;
    41     for(register int i=1;i<=m;++i) scanf("%d",&aa[i]),b[i]=aa[i];
    42     sort(b+1,b+m+1);
    43     for(register int i=1;i<=m;++i)
    44         a[i]=lower_bound(b+1,b+m+1,aa[i])-b;
    45     dfs(n,0);
    46     printf("%d",A(ans));
    47 }
    View Code

    100%:

      我没想到正解竟是暴搜。。先将阶乘与2n预处理出来(上文已提到),再进行深搜,并按照从小到大的操作顺序进行操作(这个其实还是比较好想到,一般的解决问题的过程也是将大任务分成若干个小任务,只有小决策先满足,大决策才可能满足,其实也就是所有小决策的满足(或最优)一定可以得到大决策的满足(或最优))。针对dfs中判断是否需要转化:现在的点所代表的一段区间与相邻的下一段进行比较,若它们并不是连续的,就记录下现在这个点;如果失配的个数超过两个,那么就一定不成立,return即可;到边界时更新答案(阶乘)。

    Q:为什么要将两段相邻的区间进行比较?

    A;首先是为了方便。。。第一,有可能就是两个相邻的区间位置不对,直接交换即可,同时也省去了。第二,节省时间复杂度与空间复杂度,像我在考试时就是O(n2)地寻找,既费空间又费时间;若像这样与下一个进行比较,O(n)解决。

    Q;为什么失配超过两个就一定不成立?

    A;我们这样相邻两个区间进行比较,说两个不成立也就是相对来说有4个不成立的区  间,我们就可以进行4次交换(设左边两个区间为a,b;右边两个区间为c,d;那么交  换的方式就有a->c,a->d,b->c,b->d(相邻的两个就不用交换了,因为若相邻两个交换了,另一对区间就无法满足,因为每一个操作只能用一次,而我们又是从小操作到大操作,接下来的操作不可能再使它满足))。交换后一定会出现有两种交换情况满足(我们只是保证在部分连续即可),一种是左边整体小于右边,另一种是右边小于左边(这种情况在下一个操作中可恢复)。

    Code:

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<cstdlib>
     4 #define $ 10000010
     5 using namespace std;
     6 int m,n,a[$],fac[123],tw[123],ans;
     7 inline bool judge(int x,int k){    
     8     for(register int i=1;i<tw[k];++i)
     9         if(a[x+i]!=a[x+i-1]+1) return 1;
    10     return 0;
    11 }
    12 inline void swap(int &x,int &y){
    13     int t=x; x=y; y=t;
    14 }
    15 inline void Swap(int x,int y,int k){
    16     for(register int i=0;i<tw[k];++i) swap(a[x+i],a[y+i]);
    17 }
    18 inline void dfs(int x,int k){
    19 //x表示用第几种操作,k表示用了几个操作
    20     if(x==n+1){    ans+=fac[k]; return;    }
    21     int opt1=0,opt2=0;
    22     for(register int i=1;i<=tw[n];i+=tw[x])
    23         if(judge(i,x)){
    24             if(opt1==0)       opt1=i;//不合法一次
    25             else if(opt2==0)  opt2=i;//不合法两次
    26             else return ;//否则,不可能满足题意
    27         }
    28     if(!opt1&&!opt2){    dfs(x+1,k); return;    }
    29     if(opt1&&!opt2){
    30         Swap(opt1,opt1+tw[x-1],x-1);
    31         dfs(x+1,k+1);
    32         Swap(opt1,opt1+tw[x-1],x-1);
    33         return;
    34     }
    35     if(opt1&&opt2){
    36         for(register int i=0;i<=1;++i)
    37             for(register int j=0;j<=1;++j){
    38                 Swap(opt1+i*tw[x-1],opt2+j*tw[x-1],x-1);
    39                 if(!judge(opt1,x)&&!judge(opt2,x)){//都满足就继续搜
    40                     dfs(x+1,k+1);
    41                     Swap(opt1+i*tw[x-1],opt2+j*tw[x-1],x-1);
    42                     break;//不是return
    43                 }
    44                 Swap(opt1+i*tw[x-1],opt2+j*tw[x-1],x-1);
    45             }
    46     }
    47 }
    48 signed main(){
    49     tw[0]=1;
    50     for(register int i=1;i<=12;++i) tw[i]=tw[i-1]<<1;
    51     fac[0]=fac[1]=1;
    52     for(register int i=2;i<=12;++i) fac[i]=fac[i-1]*i;
    53     scanf("%d",&n);
    54     for(register int i=1;i<=tw[n];++i) scanf("%d",&a[i]);
    55     dfs(1,0);
    56     //针对为什么x==1,其实我们需要先将小块恢复,再进行大块的恢复
    57     printf("%d",ans);
    58 }
    View Code
    越努力 越幸运
  • 相关阅读:
    编程里的数字游戏
    SqlServer2005Express下的事件探查器
    来玩玩这个
    把一个bitmap在内存中的数据块 搬到另一个bitmap中
    一些常用的sql
    C#图像处理
    来玩玩画直线
    关于未来的思考
    年末个人小结
    [转载]编写超级可读代码的15个最佳实践
  • 原文地址:https://www.cnblogs.com/OI-zzyy/p/11150002.html
Copyright © 2020-2023  润新知