题意
有一个 (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) imes 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
",ans%MOD);
return 0;
}