【SDOI2015】排序(dfs+结论)
仔细审题:
两个操作序列不同,当且仅当操作个数不同,或者至少一个操作不同(种类不同或者操作位置不同(注意,这里的“操作位置”指的是操作顺序))。
也就是说操作序列对答案的贡献多与少和你某一次操作交换哪两段数字没有关系。
还有一个:
每种操作最多可以执行一次。
这是一个很重要的条件。
考场上手玩了一下样例,发现操作结果和操作顺序没有关系(结论1,证明是赛后才想出来的)。
那么我们把操作种类都相同的操作序列都归为一类。显然,根据结论1,如果某一类中的某一个操作序列(设其长度为 \(len\))能实现将数组 \(A\) 从小到大排序,那么这一类的所有操作序列(显然,这一类的所有操作序列都是 \(len\))都可以实现将数组 \(A\) 从小到大排序,那么这一类的所有操作序列对答案的贡献是 \(len!\)。
那么对于一类操作序列,我们就先钦定操作序列的操作种类是从小到大的,然后再看能不能满足题意。比如说对于一类操作序列 \(\{132,123,213,231,312,321\}\),我们就看操作序列 \(123\) 能不能满足题意。
考虑用 dfs 枚举每一种操作做不做,最后得出一个操作种类从小到大排的操作序列,时间复杂度 \(O(2^n)\)。
关键是如何判断一种操作序列能不能满足题意。
不妨设当前的操作序列是 \(k_1,k_2,\dots,k_m\),有 \(k_1<k_2<\dots<k_m\)。
接下来的时间点可能会有点复杂,请耐心阅读。
假设我们现在要执行操作 \(k_i\),那么就要将序列从左到右划分为 \(part=2^{n-k_i+1}\) 段,每段恰好包括 \(len=2^{k_i-1}\) 个数,然后整体交换其中两段。那么有:
-
如果某一段内的数不是已经排好序的话,这种操作序列不可能满足题意,因为之后的操作都比这一次操作大,段内的顺序是不可能再改变的了。
-
如果某一段的开头不是 \(len\times t+1 (t\in \mathbb{N})\) 的话,这种操作也不可能满足题意。
假设我们现在要执行操作 \(k_i\),且满足上面的两个条件,那么我们现在要为下一次执行操作做准备,也就是说要把每段内的数排好序(这里的“段”是指按操作 \(k_i+1\) 分的段)。
也就是说要保证:把当前数组分的段两两合并成一段后,每一段内是要排好序的。比如当前 \(56|78|34|12\)(“\(|\)”是分段的意思),两两合并成一段后得保证每段内要排好序,也就是要把当前的第 \(1\)、\(2\) 段交换,再两两合并:\(56|78|34|12\rightarrow 56|78|12|34 \rightarrow 5678|1234\)。
那么我们可以先按照下一次操作的分法,得出有哪些段(这里的“段”是指按操作 \(k_i+1\) 分的段)是不符合要求的,然后分情况讨论:
-
有大于 \(2\) 段不符合条件,不能满足题意,因为一次交换操作最多只能改变两段。
-
所有段都符合条件,那么不用执行操作 \(k_i\),直接向下递归。
-
只有 \(1\) 段不符合条件,那么就把这段的两个小段(小段是指按操作 \(k_i\) 分的段)前后交换,因为我们已经保证了这两个小段是满足条件的。
-
恰有 \(2\) 段不符合条件,设这两段为 \(x\)、\(y\),\(x\) 分出来的两个小段为 \(a\)、\(b\),\(y\) 分出来的两个小段为 \(c\)、\(d\)。那么我们分四种情况看符不符合题意:\(\operatorname{swap}(a,c)\)、\(\operatorname{swap}(a,d)\)、\(\operatorname{swap}(b,c)\)、\(\operatorname{swap}(b,d)\)。
最后一直到 \(k_i=n\) 都满足条件的话,就可以记录并返回了。
因为我们是按操作种类来枚举的,所以每个答案都要阶乘一下,而且要用状压记录一下操作种类防止算重(代码中有注释)。
代码如下:
#include<bits/stdc++.h>
#define P 4100
#define ll long long
using namespace std;
ll ans;
int n,pown,a[P];
bool vis[P];//状压记录每个序列种类
ll A(int x)//阶乘
{
ll ans=1;
for(int i=1;i<=x;i++)
ans*=i;
return ans;
}
void swapp(int st1,int ed1,int st2,int ed2)//将A[st1,ed1]与A[st2,ed2]交换
{
for(int l=st1,r=st2;l<=ed1;l++,r++)
swap(a[l],a[r]);
}
bool check(int st,int ed,int len)//看A[st,ed]是否满足题解中说到的两个条件
{
if((a[st]-1)%len) return false;//条件2
for(int i=st+1;i<=ed;i++)
if(a[i]!=a[i-1]+1)//条件1
return false;
return true;
}
void dfs(int k,int sum,int sta)
{
if(k==n)//退出
{
if(!vis[sta])//判断防止算重
{
ans+=A(sum);
vis[sta]=true;
}
return;
}
int len=1<<(k+1),part=1<<(n-k-1);
vector<int>wrong;//有哪些段是不符合条件的
wrong.clear();
for(int i=1;i<=part;i++)
{
int st=(i-1)*len+1,ed=i*len;
if(!check(st,ed,len)) wrong.push_back(i);
}
if(wrong.size()>2) return;//大于2不符合题意
if(!wrong.size())//等于0直接向下递归
{
dfs(k+1,sum,sta);
return;
}
if(wrong.size()==1)//等于1交换前后两个小段
{
int id=wrong[0];
int st=(id-1)*len+1,ed=id*len,mid=(st+ed)/2;
swapp(st,mid,mid+1,ed);
dfs(k+1,sum+1,sta|(1<<k));
swapp(st,mid,mid+1,ed);
return;
}
//等于2分四种情况讨论
int id1=wrong[0],id2=wrong[1];
int st1=(id1-1)*len+1,ed1=id1*len,mid1=(st1+ed1)/2;
int st2=(id2-1)*len+1,ed2=id2*len,mid2=(st2+ed2)/2;
swapp(st1,mid1,st2,mid2);
if((check(st1,ed1,len)&&check(st2,ed2,len))) dfs(k+1,sum+1,sta|(1<<k));
swapp(st1,mid1,st2,mid2);
swapp(st1,mid1,mid2+1,ed2);
if((check(st1,ed1,len)&&check(st2,ed2,len))) dfs(k+1,sum+1,sta|(1<<k));
swapp(st1,mid1,mid2+1,ed2);
swapp(mid1+1,ed1,st2,mid2);
if((check(st1,ed1,len)&&check(st2,ed2,len))) dfs(k+1,sum+1,sta|(1<<k));
swapp(mid1+1,ed1,st2,mid2);
swapp(mid1+1,ed1,mid2+1,ed2);
if((check(st1,ed1,len)&&check(st2,ed2,len))) dfs(k+1,sum+1,sta|(1<<k));
swapp(mid1+1,ed1,mid2+1,ed2);
}
int main()
{
scanf("%d",&n);
pown=1<<n;
for(int i=1;i<=pown;i++)
scanf("%d",&a[i]);
dfs(0,0,0);
printf("%lld\n",ans);
return 0;
}
/*
2
1 3 2 4
*/
/*
3
3 6 1 2 7 8 5 4
*/
/*
2
1 4 2 3
*/