[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
越努力 越幸运
原文地址:https://www.cnblogs.com/OI-zzyy/p/11150002.html