题目传送门
洛谷 P5664
前言
本题解为作者整合了自己学习其他题解后为自己写的用以复习的笔记,不喜勿喷谢谢,但是有逻辑错误或语言不清晰之处欢迎提出!
题目描述
Emiya 是个擅长做菜的高中生,他共掌握 \(n\) 种烹饪方法,且会使用 \(m\) 种主要食材做菜。为了方便叙述,我们对烹饪方法从 \(1 \sim n\) 编号,对主要食材从 \(1 \sim m\) 编号。
Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 \(a_{i,j}\) 道不同的使用烹饪方法 \(i\) 和主要食材 \(j\) 的菜(\(1 \leq i \leq n\)、\(1 \leq j \leq m\)),这也意味着 Emiya 总共会做 \(\sum\limits_{i=1}^{n} \sum\limits_{j=1}^{m} a_{i,j}\) 道不同的菜。
Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 \(k\) 道菜的搭配方案而言:
- Emiya 不会让大家饿肚子,所以将做至少一道菜,即 \(k \geq 1\)
- Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
- Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 \(\lfloor \frac{k}{2} \rfloor\) 道菜)中被使用
这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。
Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 \(998,244,353\) 取模的结果。
这里的 \(\lfloor x \rfloor\) 为下取整函数,表示不超过 \(x\) 的最大整数。
题意简化
给定一个 \(n\) 行 \(m\) 列的矩阵 \(a\) ,每行 \(i\) 中至多选择某一列 \(j\) 的 \(a_{i,j}\) 种可能中的1个,求有多少种方案满足以下3个约束。
- 至少选择一个
- 每行至多选一个
- 每列最多选总数的一半
思路
直接计数dp。目标时间复杂度: \(\text O(mn^2)\)
注意到约束3过于讨嫌难以处理,所以 正难则反 ,考虑容斥原理:
对于约束3,若违反,则说明此列中有超过一半的菜,所以其他列肯定是少于一半,符合要求的。
既然如此,我们就枚举是哪一列违反了约束3,统计这些不合法方案总数,最后用总方案数减去即得答案。
统计总方案数
具体的,令 \(tot[i][j]\) 表示前 \(i\) 行中,选择了 \(j\) 次的方案数,注意 \(j\) 次等价于选了 \(j\) 行,因为每行只能选一个。
则转移方程考虑两种情况:
- 这一行没选
- 这一行选了某一个菜
故有:
,其中 \(s[i]\) 表示第 \(i\) 行中的总菜数,即
统计非法方案
对于第 \(col\) 列,令 \(dp[i][j][k]\) 代表前 \(i\) 行中选了 \(j\) 个 \(col\) 的菜品,选了 \(k\) 个其他菜的菜品。
则考虑三种情况:
- 这一行啥都没选
- 这一行选了 \(col\) ,总共有 \(a[i][col]\) 种可能选择,乘法原理
- 这一行没选 \(col\) 但选了其他的,总共有 \(s[i]-a[i][col]\) 种可能选择,同样乘法原理开大怼上去
于是有:
优化
这个优化比较玄学,也是我写这篇题解的主要搞懂对象。
注意到非法方案满足 \(j>k\) ,即 \(j-k>0\)
换言之,我们只需维护 \(j-k\) ,不需要真正关注 \(j,k\) 的值。
那么,令 \(dp[i][\triangle x]\) 代表前 \(i\) 行中,选了菜品 \(col\) 的减去选了菜品 \(col\) 之外的值为 \(\triangle x\) 的方案数。
转移方程需考虑三种情况:
- 这一行啥都没选,直接继承 \(dp[i-1][\triangle x]\)
- 这一行选了 \(col\) ,\(\triangle x\) 加 \(1\)
- 这一行选了 \(col\) 之外的, \(\triangle x\) 减 \(1\)
如何证明这囊括了所有情况,不重不漏呢?
这个我也不会,如果有知道的麻烦在评论区不吝赐教,谢谢!
总之,新的转移方程挺显然的(用 \(j\) 代替 \(△x\)):
最后,注意到 \(dp[i][j]\) 中的 \(-n\leq j\leq n\) ,所以给 \(dp[i][j]\) 的 \(j\) 强行都加 \(n\) ,
那么最后符合非法方案的就从 \(j>0\) 变成 \(j>n\) ,即最后减方案数减的是
AC 代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 998244353;
const int N = 105, A = 2005;
int n, m;
ll a[N][A], s[N];
// tot[i][j]: 前i种烹饪方法中选了j个食材的总方案数
ll tot[N][N];
// dp[i][j+n]: 前i种烹饪方法中超限的食材出现次数减去其他食材出现次数为j的非法方案数
ll dp[N][N * 2];
int main()
{ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m;for (int i = 1; i <= n; ++i) {for (int j = 1; j <= m; ++j) {cin >> a[i][j];(s[i] += a[i][j]) %= M;}// printf("There are %lld ways to cook with way %d\n", s[i], i);}tot[0][0] = 1;for (int i = 1; i <= n; ++i) {tot[i][0] = tot[i - 1][0];for (int j = 1; j <= n; ++j) {tot[i][j] = tot[i - 1][j] + tot[i - 1][j - 1] * s[i] % M;tot[i][j] %= M;// printf("There's %lld ways to pick %d columns from the first %d rows.\n", tot[i][j], i, j);}}// printf("tot:\n");// for (int i = 1; i <= n; ++i) {// for (int j = 0; j <= n; ++j) {// printf("%lld\t", tot[i][j]);// }// printf("\n");// }ll ans = 0;for (int col = 1; col <= m; ++col) {// printf("If %d is the invalid ingredient:\n", col);memset(dp, 0, sizeof(dp));dp[0][n] = 1;for (int i = 1; i <= n; ++i) {for (int j = n - i; j <= n + i; ++j) {dp[i][j] = dp[i - 1][j];(dp[i][j] += dp[i - 1][j - 1] * a[i][col] % M) %= M;(dp[i][j] += dp[i - 1][j + 1] * (s[i] - a[i][col]) % M) %= M;// for (int k = 1; k <= m; ++k) {// dp[i][j][k] = dp[i - 1][j][k] + dp[i - 1][j - 1][k] * a[i][j] + dp[i - 1][j][k - 1] * (s[i] - a[i][j]);// }}}// printf("DP:\n");// for (int i = 1; i <= n; ++i) {// for (int j = n - i; j <= n + i; ++j) {// printf("dp[%d][%d]: %lld\t", i, j - n, dp[i][j]);// }// printf("\n");// }for (int i = 1; i <= n; ++i)(ans -= dp[n][i + n]) %= M;ans = (ans % M + M) % M;}for (int i = 1; i <= n; ++i) {(ans += tot[n][i]) %= M;}cout << ans << '\n';return 0;
}
后记
这里其实可以滚一下,把dp第一维压到2,用奇偶滚一下的说但是没必要
证明差分dp可行目前我能想到的只有暴力打印每次迭代的dp数组去看是不是满足j-k为定值的都相等/加起来怎么怎么样的,有知道具体方法的务必留一下,谢谢!