CF1466H - Finding satisfactory solutions
CF1466H - Finding satisfactory solutions
题目大意
给定一组置换\(a_i\),现在对于每个元素\(i\)构造一个排列\(p_i\)
有两类边
1.\(i\rightarrow a_i\)
2.表示\(i\)向\(p_i\)中所有在\(i\)前面出现的数\(p_{i,j}\)连有向边
现在要求最终的图里没有2类边出现在任何一个环上
\(n\leq 40\)
图分析
置换显然构成一堆环,答案只与每个置换环的大小有关
把置换环缩点,2类边就构成置换环之间的拓扑图
计算答案
由于连边方式比较奇怪,\(i\)向任何\(j\)个点连边的方案数是\(j!(n-1-j)!\)
考虑对于缩点之后的点进行拓扑图\(dp\),这是一个经典问题,参考 [CEOI2019] 游乐园 | [CEOI2019] Amusement Park
因此需要一个枚举子集的过程
而由于这题的特殊性,每个点实际上只与其对应置换环答案有关
把一类点放在一起计算,枚举时额外乘上一个组合数即可
实际实现时并不需要把所有点分类,可以只把大小为1,2的点拿出来,剩下的直接状压
const int N=42,P=1e9+7;
int n,m;
int p[N],vis[N],C[N][N];
int a[N],W[N][N],J[N],sz[1<<14];
vector<vector<vector<int>>> dp; // 爽!!!
int main() {
n=rd();
rep(i,1,n) p[i]=rd();
rep(i,0,n) rep(j,*C[i]=1,i) C[i][j]=(C[i-1][j-1]+C[i-1][j])%P;
rep(i,*J=1,n) J[i]=1ll*J[i-1]*i%P;
int c1=0,c2=0;
rep(i,1,n) if(!vis[i]) {
int c=0;
for(int j=i;!vis[j];j=p[j]) vis[j]=1,c++;
if(c==1) c1++;
else if(c==2) c2++;
else a[m++]=c;
}
rep(i,0,n-1) {
int t=0;
rep(j,0,i) t=(t+1ll*C[i][j]*J[j]%P*J[n-j-1])%P;
rep(j,*W[i]=1,n) W[i][j]=1ll*W[i][j-1]*t%P;
}
int A=(1<<m)-1;
rep(i,1,A) sz[i]=sz[i&(i-1)]+a[__lg(i&-i)];
dp.resize(c1+1);
rep(i,0,c1) {
dp[i].resize(c2+1);
rep(j,0,c2) dp[i][j].resize(A+1);
}
dp[0][0][0]=1;
rep(i,0,c1) rep(j,0,c2) rep(S,0,A) {
int c=sz[S]+i+2*j;
rep(a,0,c1-i) rep(b,0,c2-j) {
int R=A^S;
for(int T=R;;T=(T-1)&R) {
if(!a && !b && !T) break;
int d=sz[T]+a+b*2,parity=(__builtin_parity(T)^a^b)&1;
dp[a+i][b+j][S|T]=(dp[a+i][b+j][S|T]+1ll*(parity&1?1:-1)*W[c][d]*dp[i][j][S]%P*C[a+i][i]%P*C[b+j][j])%P;
if(T==0) break;
}
}
}
int ans=(dp[c1][c2][A]+P)%P;
printf("%d\n",ans);
}