P5664[CSP-S2019] Emiya 家今天的饭 (dp + 计数)
题目大意:
给定一个大小为 \(n * m\) 的表格 , 其中 \(a_{i , j}\) 表示用第 \(i\) 种烹饪方式并且有第 \(j\) 种主要食材的不同菜品的数量,找出至少有一种菜品,每种菜品的烹饪方式不同且满足,所有的主要食材的出现次数不超过总菜品数的一半的方案数。
题目分析:
-
[\(1\)] : 因为每种菜品的烹饪方式均不同,那么我们想到可以枚举烹饪方式。
-
[\(2\)] : 正难则反。我们对于方案数可以很快速的求出总的方案数,那我么可以想到用总的方案数减去不符合的方案数。
-
[\(3\)] : 设总方案数为 \(ans\) , 那么我们先考虑每一行的方案数,对于每一行只能选取一种,故每行方案数为这种烹饪方式所能做的所有菜品,我们令 \(sum_i\) 为每行的总数量,那么 \(ans\) 是否等于 \(\prod_{i = 1} ^ n sum_i\) 呢? 答案是否定的,因为对于每一行我们还可以不选,所以 \(ans = \prod_{i = 1} ^ n ({sum_i +1})\) , 在 \(ans\) 中还包含了一个什么也不选的方案,所以还需要将 \(ans\) 减 \(1\)。
-
[\(4\)] : 考虑完第一个以及第二个条件,我们再来考虑不满足第三个条件的方案数,由于遍历有一定的顺序,所以我们考虑使用 \(dp\) 来求解 。
-
[\(5\)] : 首先我们考虑最暴力的做法,根据什么不好处理就将其放到状态中的方法, 我们令 \(f_{i , j, k , l}\) 为前 \(i\) 中烹饪方式中,选了 \(k\) 个菜品 , \(j\) 食材出现了 \(l\) 次的方案数。 那么很好得出: \(f_{i,j,k,l} = f_{i - 1 , j,k,l} + a_{i,j} * f_{i - 1,j,k - 1,l - 1} + (\Sigma_{x=1}^n a_{i,x} - a_{i,j}) * f_{i - 1,j,k - 1,l}\)。 其中 \(\Sigma_{x=1}^n a_{i,x} - a_{i,j}\) 可以变为 \(sum_i - a_{i,j}\)。所以 : \(f_{i,j,k,l} = f_{i - 1 , j,k,l} + a_{i,j} * f_{i - 1,j,k - 1,l - 1} + (sum_i - a_{i,j}) * f_{i - 1,j,k - 1,l}\)
复杂度为 \(O(n^3m)\) -
[\(6\)] : 根据转移式子我们可以发现,注意的复杂度已经降为 \(O(1)\) ,故我们考虑去优化状态,因为题目中要求最多的食材不超过所选菜品的一半,所以我们直接维护这种食材与其他食材的差即可 ,所以我们令 \(f_{i,j,k}\) 为前 \(i\) 中烹饪方式,\(j\) 食材的数量与其他所有食材的数量之差为 \(k\)。 那么转移方程为: \(f_{i,j,k} = f_{i - 1,j,k} + a_{i,j} * f_{i - 1,j,k - 1} + (sum_i - a_{i,j}) * f_{i - 1,j,k + 1}\),我们又发现每次转移时 \(j\) 是不发生变化的,所以我们考虑将 \(j\) 这一维去掉,我们可以在最外层枚举第 \(j\) 种食材,然后就降维了,最后我们只需要让 \(ans\) 见到所有 \(k > 0\) 的情况即可。
代码实现:
因为 \(k\) 可能会出现负的,所以我们让 \(k\) 都去加上 \(n\) 来保证 \(k\) 都是正的,其他细节见代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
const int M = 2e3 + 7;
int a[200][M] , sum[200];
int n , m;
int dp[200][500];
int get_id(int x) {//这里就是将 k 变为正的的
return x + n + 1;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++ i) for(int j = 1; j <= m; ++ j) {cin >> a[i][j];sum[i] += a[i][j];sum[i] %= mod;}
int ans = 1;
for(int i = 1; i <= n; ++ i) ans = (ans * (sum[i] + 1)) % mod;//求出总方案数
ans = (ans - 1 + mod) % mod;//减掉一个都不选的
for(int i = 1; i <= m; ++ i) {
memset(dp , 0 , sizeof dp);
dp[0][get_id(0)] = 1;//初值
for(int j = 1; j <= n; ++ j)
for(int k = -1 * n; k <= n; ++ k)
dp[j][get_id(k)] = (dp[j - 1][get_id(k)] + a[j][i] * dp[j - 1][get_id(k - 1)] % mod + ((sum[j] + mod - a[j][i]) * dp[j - 1][get_id(k + 1)]) % mod) % mod;
for(int k = 1; k <= n; ++ k) ans = (ans - dp[n][get_id(k)] + mod) % mod;
}
cout << ans;
return 0;
}
[========]