2019CSP-S D2T1Emiya家今天的饭(动态规划)

隔了(鸽了)半年多,重新回来做这道题,还是感觉很难… …果然蒟蒻还是蒟蒻… …

https://www.luogu.com.cn/problem/P5664

题意:给一个 n ∗ m n*m nm的矩阵,限制每行最多只能取一个点,每列所取的点数不能超过总点数的 1 2 \frac{1}{2} 21 ,取的总点数不能为0,求总方案数。

32 p t s 32pts 32pts

n ≤ 10 , m ≤ 3 n\le10,m\le3 n10m3
暴力枚举。
复杂度 O ( 4 n ) O(4^n) O(4n) 。(3种菜+不选)

//省掉了头文件、变量定义和读优,最后AC代码里有
void dfs(int k,long long sum,int x,int y)
//k是当前枚举到的行,sum是目前统计的总个数
//x是共要选的行数(要选菜的个数),y是已经选了的行数
{
	if(x==y) {ans=(ans+sum)%mod;return;}
	if(k==n+1) return;
	for(int i=1;i<=m;i++)
	{
		if(q[i]+1<=x/2)
		{
			q[i]++;
			dfs(k+1,sum*a[k][i]%mod,x,y+1);
			q[i]--;
		}
	}
	dfs(k+1,sum,x,y);
}

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) a[i][j]=read();
	for(int i=2;i<=n;i++) dfs(1,1,i,0);  //从2开始枚举,因为只做一道菜绝对不合法
	printf("%lld",ans);
	return 0;
}

64 p t s 64pts 64pts

n ≤ 40 , m ≤ 3 n\le40,m\le3 n40m3
虽然暴力枚举不行了,但我们可以dp(一道dp好题怎能不用dp??? ) 。
考虑简单的dp。
开四维数组 f [ i ] [ j ] [ k ] [ p ] f[i][j][k][p] f[i][j][k][p],表示前 i i i行,选了 j j j个菜品1, k k k个菜品2, p p p个菜品3。
背包走起, O ( n 4 ) O(n^4) O(n4)

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) a[i][j]=read();
	f[0][0][0][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=i;j++)
			for(int k=0;k<=i;k++)
				for(int p=0;p<=i;p++)
				{
					f[i][j][k][p]=(f[i][j][k][p]+f[i-1][j][k][p])%mod;
					if(j) f[i][j][k][p]=(f[i][j][k][p]+f[i-1][j-1][k][p]*a[i][1]%mod)%mod;
					if(k) f[i][j][k][p]=(f[i][j][k][p]+f[i-1][j][k-1][p]*a[i][2]%mod)%mod;
					if(p) f[i][j][k][p]=(f[i][j][k][p]+f[i-1][j][k][p-1]*a[i][3]%mod)%mod;
				}
	for(int i=0;i<=n/2;i++)
		for(int j=0;j<=n/2;j++)
			for(int k=0;k<=n/2;k++)
				if(i<=k+j&&j<=i+k&&k<=i+j)
					ans=(ans+f[n][i][j][k])%mod;
	printf("%lld",ans-1);
	return 0;
}

100 p t s 100pts 100pts

好了,到目前我们已经得到了64分,对于我这种蒟蒻在考场上就已经满足走人了,然而这不是考场,我们还是要写出正解。
n ≤ 100 , m ≤ 2000 n\le100,m\le2000 n100m2000
此时,我们就要逆向思考了。
我们可以轻易得出:合法的情况数=总情况数–不合法的情况数。
总情况数可以预处理(见代码)。
不合法的情况就是有一列选的点数大于总点数的 1 2 \frac{1}{2} 21,我们可以枚举这个超过了 1 2 \frac{1}{2} 21的列 k k k,然后开二维数组 f [ i ] [ j + 100 ] f[i][j+100] f[i][j+100],表示dp到第 i i i行, k k k列与其它列点数之和的差,加100是为了避免第二维为负数RE
然后就可以快乐的写状态转移方程啦:
f [ i ] [ j + 100 ] = ( f [ i − 1 ] [ j + 100 ] + f [ i − 1 ] [ j + 1 + 100 ] ∗ B [ i ] ) m o d    M o d ; f[i][j+100]=(f[i-1][j+100]+f[i-1][j+1+100]*B[i])\mod Mod; f[i][j+100]=(f[i1][j+100]+f[i1][j+1+100]B[i])modMod;
i f ( j + 100 − 1 ≥ 0 ) f [ i ] [ j + 100 ] = ( f [ i ] [ j + 100 ] + f [ i − 1 ] [ j + 100 − 1 ] ∗ A [ i ] ) m o d    M o d ; if(j+100-1\ge0)f[i][j+100]=(f[i][j+100]+f[i-1][j+100-1]*A[i])\mod Mod; if(j+10010)f[i][j+100]=(f[i][j+100]+f[i1][j+1001]A[i])modMod;
f [ i − 1 ] [ j + 100 ] f[i-1][j+100] f[i1][j+100]是什么都不选, f [ i − 1 ] [ j + 1 + 100 ] ∗ B [ i ] f[i-1][j+1+100]*B[i] f[i1][j+1+100]B[i]是选除 k k k列外其它点, f [ i − 1 ] [ j + 100 − 1 ] ∗ A [ i ] f[i-1][j+100-1]*A[i] f[i1][j+1001]A[i]是选 k k k列上的点。
时间复杂度 O ( m n 2 ) O(mn^2) O(mn2)

#include
using namespace std;

int read()
{
	int i=0;char ch;
	while(!isdigit(ch)) ch=getchar();
	while(isdigit(ch)) {i=i*10+ch-'0';ch=getchar();}
	return i;
}

const int mod=998244353;
int n,m,a[105][2005],s[105];
long long ans=1,sum,f[105][2005],A[105],B[105];

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) a[i][j]=read();
	for(int i=1;i<=n;i++)  //预处理总情况
	{
		for(int j=1;j<=m;j++)
			s[i]=(s[i]+a[i][j])%mod;
		ans=ans*(s[i]+1)%mod;
	}
	for(int k=1;k<=m;k++)
	{
		for(int i=1;i<=n;i++)
		{
			A[i]=a[i][k];  //当前行属于第k列的情况数
			B[i]=(s[i]-a[i][k]+mod)%mod;//当前行不属于第k列的总情况数(加mod以防负数)
		}
		f[0][100]=1;
		for(int i=1;i<=n;i++)
		{
			for(int j=-i;j<=i;j++)
			{
				f[i][j+100]=(f[i-1][j+100]+f[i-1][j+1+100]*B[i])%mod;
				if(j+100-1>=0) f[i][j+100]=(f[i][j+100]+f[i-1][j+100-1]*A[i])%mod;
			}
		}
		for(int i=1;i<=n;i++) sum=(sum+f[n][i+100])%mod;
	}
	printf("%lld",(ans-sum-1+mod)%mod);  //-1除掉都不选的方案,+mod以防负数
	return 0;
}

你可能感兴趣的:(动态规划)