题解 AT4515 [AGC030F] Permutation and Minimum
题意
有一个 \(2 N\) 个数的序列 \(A\),从 \(1\) 到 \(2 N\) 标号。你要把 \(1 \sim 2 N\) 这些数填进去,使它形成一个排列。
但是已经有一些位置强制填了特定的数了,输入时会给出。
最后令长度为 \(N\) 的序列 \(B\) 为:令 \(B_i = \min\{A_{2 i - 1}, A_{2 i}\}\)。
询问所有方案中能得到的不同的 \(B\) 的数量。
\(1 \le N \le 300\)
思路
我们把\((A_{2i},A_{2i-1})\)称作第\(i\)对数。显然,一对数只有至少一个数为-1,才会对答案有贡献
接着就是这些有答案贡献的对填空,将没有填的数字从大到小排序来考虑
为什么是从大到小填?假设从小到大填,假设填了一个数对\((i,-1)\),你会发现这个数对无论填\(i+1\)还是\(i+2\),实际上方案数并没有增加,而我们会重复计算,具有后效性。但从大到小并没有这种担忧,可以始终保证答案不重不漏
设\(f(i,j,k)\)表示当前考虑到第\(i\)个数,还剩\(j\)个只填了1个数的对(不包含被限定一位的对),称作\((-1,a)\),\(k\)个原本就限定了一位的对,称作\((-1,x)\)
考虑前\(i-1\)个数已经讨论,现在考虑第\(i\)个数,进行分类讨论
如果它是\((-1,x)\)中的\(x\)(也就是被限定的数字),那么它可以不匹配,\(f(i-1,j,k)\)转移到\(f(i,j,k+1)\),也可以匹配与一个填了一个数的\((-1,a)\)匹配,那么\(f(i,j,k)\)转移到\(f(i,j-1,k)\)
如果不是,那么它可以不匹配,\(f(i-1,j-1,k)\)转移到\(f(i,j,k)\);也可以填入\((-1,a)\),\(f(i-1,j+1,k)\)转移到\(f(i,j,k)\);还可以填入\((-1,x)\),因为任意\(x\)的位置固定,所以会有\(k+1\)种填入方案,\((k+1)\times f(i-1,j,k+1)\)转移到\(f(i,j,k)\)
然后我们就讨论完了,初状态\(f(0,0,0)=1\)
又因为原本都是-1的数对是无序的,所以最终答案还得乘上原本都是-1的数对个数的阶乘
感悟
从这一题中我们可以发现,设计状态转移方程的时候,不一定要以“如何计算出某一个状态入手”,也可以换一个思路,思考“这一个状态可以更新哪些后面的状态”,根据具体情况来选择更加简单,顺畅的思路
代码
#include<bits/stdc++.h>
using namespace std;
int const MAXN=610,MOD=1e9+7;
int n,cnt1,cnt2,m,ans=1;
int a[MAXN],vis[MAXN],f[MAXN][MAXN][MAXN];
void add(int &x,int y){x+=y;if(x>=MOD)x-=MOD;}
int main(){
scanf("%d",&n);n*=2;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i+=2){
if(a[i]>-1 &&a[i+1]>-1){
vis[a[i]]=vis[a[i+1]]=2;
}else if(a[i]<0 && a[i+1]<0){
cnt1++;
}else{
cnt2++;
if(a[i]>-1)vis[a[i]]=1;
if(a[i+1]>-1)vis[a[i+1]]=1;
}
}
for(int i=n;i>=1;i--){
if(vis[i]<2)a[++m]=i;
}
f[0][0][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++){
for(int k=0;k<=n;k++){
if(vis[a[i]]==1){
if(k)add(f[i][j][k],f[i-1][j][k-1]);
add(f[i][j][k],f[i-1][j+1][k]);
}else{
if(j)add(f[i][j][k],f[i-1][j-1][k]);
add(f[i][j][k],f[i-1][j+1][k]);
add(f[i][j][k],1ll*f[i-1][j][k+1]*(k+1)%MOD);
}
}
}
}
ans=f[m][0][0];
for(int i=1;i<=cnt1;i++)ans=1ll*ans*i%MOD;
printf("%d\n",ans%MOD);
return 0;
}