CSP-S2 2019 D2T1
很不錯的一題DP,通過這道題學到了很多。
身為一個對DP一竅不通的蒟蒻,在考場上還掙扎了1h來推式子,居然還有幾次幾乎推出正解,然而最后還是只能打個32分的暴搜滾粗
題意分析
給出一個矩陣,要求每行只能選一個節點,每列選的節點不能超過所有選的節點的一半,不能不選,給出每個節點的選擇方案數,求總方案數
思路分析
可以看出,維護每列已選的節點復雜度太大,不太可行;因此很容易想到,先不考慮每列不超過一半的這個限制,求出總方案數,然后再減去考慮這個限制后不合法的方案數。現在問題就變成,求任意列選的節點超過所有選的節點的一半的方案數之和。
顯然,在一個方案中,只可能有一列的節點超過所有選的節點的一半。因此可以想到枚舉這個超過限制的列,然后對於這個列進行DP求解。
具體實現
設$f_{i,j,k}$表示前$i$行選$j$個節點,當前枚舉到的列選$k$個節點的方案數。對於每個列,復雜度為$O(n^3)$,總的復雜度為$O(mn^3)$,可以得到84分的高分。
想得到滿分還需要進一步優化。考慮將某兩個狀態合並。觀察狀態,實際上我們想知道的只是$j,k$的大小關系,對於具體的值並不關心,考慮將它們合並到一維。
考慮我們需要的限制條件$k>\left \lfloor \frac{j}{2} \right \rfloor$,變形一下可以得到$2k+n-j>n$。觀察這個式子,可以發現,$n-j$就是這$n$行里沒有選的行數。然后一個奇妙的想法就出來了,對於每個節點,選它時當做該列選了兩次,而對於某一行不選時,當做所有列選了一次,最終要找的就是當前列被選超過$n$次的方案。這樣就成功地優化掉了第二維。
給一下狀態轉移方程:
f[j][k]=(f[j][k]+f[j-1][k]*(cnt[j]-w[j][i]))%P;//不選當前列
f[j][k+1]=(f[j][k+1]+f[j-1][k])%P;//不選當前行
f[j][k+2]=(f[j][k+2]+f[j-1][k]*w[j][i])%P;//選當前行當前列對應的節點
注意取模時出現負數的情況,記得開long long。
#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
const int N=200,M=3000,P=998244353;//FFT(霧
int n,m;
ll ans=1;
ll cnt[N],w[N][M],f[N][M];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
scanf("%lld",&w[i][j]),cnt[i]=(cnt[i]+w[i][j])%P;
ans=(ans*(cnt[i]+1))%P;//計算全部答案
}
ans=(ans+P-1)%P;//減去全部不選的情況
for(int i=1;i<=m;i++)
{
memset(f,0,sizeof(f));
f[0][0]=1;//DP初值
for(int j=1;j<=n;j++)
for(int k=0;k<=2*(j-1);k++)
{
f[j][k]=(f[j][k]+f[j-1][k]*(cnt[j]-w[j][i]))%P;
f[j][k+1]=(f[j][k+1]+f[j-1][k])%P;
f[j][k+2]=(f[j][k+2]+f[j-1][k]*w[j][i])%P;
}
for(int j=n+1;j<=2*n;j++)
ans=(ans+P-f[n][j])%P;//減去當前枚舉到的不合法方案
}
printf("%lld",ans);
return 0;
}