简述题意:
给定一个矩阵,n行m列,每一行最多选1个位置打上标记,且不存在某一列所有有标记的点的数量大于全部的一半,至少给一个点打上标记,求方案数。答案对998244353取模。
很水的暴力dp可以得84分,但100分的优化做法值得很多题借鉴。
先说简单说一下暴力:
我们直接求满足上面条件的方案数并不容易,所以进行问题转化,转化为:(每行选不超过一个的方案数-每行选不超过一个且某一列选了超过一半的方案数)。
至于每行选不超过一个的方案数很容易得到:设$g[i][j]$表示前i行共选j的数的且每行选不超过一个的方案数,得到转移方程:$g[i][j]=g[i-1][j]+s_i*g[i-1][j-1]$。其中$s_i$表示第i行的数值和。
对于每行选不超过一个且某一列选了超过一半的方案数:枚举超过总数一半的某一列x:设$f[i][j][k]$表示前i行中第x列选了j个,非第x列选了k个的符合要求的方案数。
容易得到转移方程:$f[i][j][k]=f[i-1][j][k]+a[i][x]*f[i-1][j-1][k]+(sum[i]-a[i][x])*f[i-1][j][k-1]$
轻松分析出:时间复杂度是$O(mn^3)$
然后便是很有用的优化:
我们注意到,这个dp方程似乎不能使用什么数据结构进行优化,而且也无法进行省略某状态等操作。但是这个方程中第二维和第三维在我们求方案数的时候并不是一定要知道数值是什么,只要知道j和k的大小关系即可。那么我们可以压缩状态:$f[i][j]$表示前i行,当前列的数比其他列的数多了j个的符合要求的方案数。也就说压缩后的第二维等与压缩前的(第二维-第一维)。
这样可以以$O(mn^2)$的复杂度得到期望100分的好成绩。
#include <bits/stdc++.h> #define inc(i,a,b) for(register int i=a;i<=b;i++) using namespace std; int n,m; const int p=998244353; long long sum[110][2100],f[110][220]; long long ans1,ans2,a[110][2100]; int main(){ cin>>n>>m; inc(i,1,n){ inc(j,1,m) scanf("%lld",&a[i][j]),sum[i][0]=(sum[i][0]+a[i][j])%p; inc(j,1,m) sum[i][j]=(sum[i][0]-a[i][j])%p; } inc(l,1,m){ memset(f,0,sizeof(f)); f[0][n]=1; inc(i,1,n) inc(j,n-i,n+i){ f[i][j]=(f[i-1][j]+f[i-1][j-1]*a[i][l]%p+f[i-1][j+1]*sum[i][l]%p)%p; } inc(j,1,n) ans2=(ans2+f[n][n+j])%p; } memset(f,0,sizeof(f)); f[0][0]=1; inc(i,1,n) inc(j,0,n) f[i][j]=(f[i-1][j]+((j>=1)?f[i-1][j-1]*sum[i][0]%p:0))%p; inc(i,1,n) ans1=(ans1+f[n][i])%p; long long ans=((ans1-ans2)%p+p)%p; cout<<ans<<endl; }