64 pts

类似 乌龟棋 的思想,由于 \(64pts\) 的 \(m <= 3\), 非常小

我们可以设一个 \(dp\),建立 \(m\) 个维度存下每种物品选了几次:

  • \(f[i][A][B][C]\) 表示前 \(i\) 种烹饪方法,第 \(1 / 2/ 3\) 种主要食材各自选了 \(A, B, C\) 道菜的方案数。

状态转移:根据题意,每种烹饪方法最多选一道菜。

  • 不做菜 \(f[i][A][B][C] += f[i - 1][A][B][C]\)
  • 做 \(1\) 道第一种主要食材的菜 : \(f[i][A][B][C] += f[i - 1][A - 1][B][C] * a_{i, 1}\)
  • 做 \(1\) 道第二种主要食材的菜 : \(f[i][A][B][C] += f[i - 1][A][B - 1][C] * a_{i, 2}\)
  • 做 \(1\) 道第三种主要食材的菜 : \(f[i][A][B][C] += f[i - 1][A][B][C - 1] * a_{i, 3}\)

答案:\(\sum_{A = 0}^{n}\sum_{B = 0}^{n}\sum_{C = 0}^{n}f[n][A][B][C]\ (max(A, B, C) <= \lfloor(A + B + C) / 2\rfloor 且 A + B + C > 0)\)


小优化:发现所有状态只会从$A, B, C <= $ 自己的转移,所以可以用类似背包优化空间的思想,从大到小枚举状态,第一维可以滚动掉。

时间复杂度

最多选 \(n\) 道菜故时间复杂度 \(O(n^{m + 1})\)

  1. #include <cstdio>
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 45, M = 6, P = 998244353;
  5. int n, m, a[N][M];
  6. typedef long long LL;
  7. int f[N][N][N];
  8. int main() {
  9. scanf("%d%d", &n, &m);
  10. for (int i = 1; i <= n; i++)
  11. for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]);
  12. f[0][0][0] = 1;
  13. for (int i = 1; i <= n; i++) {
  14. for (int A = i; ~A; A--) {
  15. for (int B = i; ~B; B--) {
  16. for (int C = i; ~C; C--) {
  17. if(A && a[i][1]) f[A][B][C] = (f[A][B][C] + (LL)f[A - 1][B][C] * a[i][1]) % P;
  18. if(B && a[i][2]) f[A][B][C] = (f[A][B][C] + (LL)f[A][B - 1][C] * a[i][2]) % P;
  19. if(m == 3 && C && a[i][3]) f[A][B][C] = (f[A][B][C] + (LL)f[A][B][C - 1] * a[i][3]) % P;
  20. }
  21. }
  22. }
  23. }
  24. int ans = 0;
  25. for (int A = n; ~A; A--) {
  26. for (int B = n; ~B; B--) {
  27. for (int C = n; ~C; C--) {
  28. int s = A + B + C;
  29. if(s > 0 && max(A, max(B, C)) <= s / 2) (ans += f[A][B][C]) %= P;
  30. }
  31. }
  32. }
  33. printf("%d\n", ans);
  34. return 0;
  35. }

84 pts

发现 \(64pts\) 后的 \(m\) 猛增,所以我们的算法一定不能具体记录每种主要食材选了多少了。

我们发现一个方案不合法,有且只会有一个主要食材 $ > $ 总数的一半,所以我们不妨考虑容斥,用所有方案数量 - 不合法数量。


求解所有方案数量

所有方案数量很好求,做一个分组背包即可:

\(f[i][j]\) 表示前 \(i\) 种烹饪方式,做了 \(j\) 道菜的方案数。

状态转移:

  • 第 \(i\) 种烹饪方式不做菜:\(f[i][j] += f[i - 1][j]\)
  • 第 \(i\) 种烹饪方法做 \(1\) 道主要食材是 \(k\) 的菜:\(f[i][j] += f[i - 1][j - 1] * a_{i, k}\)

所有方案数量 $ = \sum_{j = 1}^{n}f[n][j]$

优化

  1. \(i, j\) 以来比它小的 \(i', j'\),第一维滚动掉
  2. 观察第二种放菜的转移:\(f[i - 1][j - 1] * a_{i, 1} + f[i - 1][j - 1] * a_{i, 2} +...+ f[i - 1][j - 1] * a_{i, m} = f[i - 1][j - 1] * (a_{i, 1} + a_{i, 2} + ... + a_{i, m})\)。我们可以 \(O(nm)\) 预处理 \(s_i = a_{i, 1} + a_{i, 2} + ... + a_{i, m}\)。每个状态即可 \(O(1)\) 转移。
这步的时间复杂度

这步有 \(n ^ 2\) 个状态,\(O(1)\) 转移。 所以时间复杂度 \(O(n ^ 2)\)

求解不合法数量

由于刚才我们发现的性质:所有不合法方案中有且只会有一个主要食材 $ > $ 总数的一半,我们称那个主要食材为越界食材,我们设越界食材为 \(c\)。

所以我们不妨先用 \(O(m)\) 枚举 \(c\) 。

那么我们可以把其他食材归结为 符合条件的食材,我们便可以用一个维度来记录它选了多少啦~

设 \(dp[i][j][k]\) 为前 \(i\) 种烹饪方式,第 \(c\) 种(越界食材)选了 \(j\) 道,其他食材选了 \(k\) 道的方案数。

状态转移:

  • 第 \(i\) 种烹饪方法不做菜:\(dp[i][j][k] += dp[i - 1][j][k]\)。\(O(1)\) 转移

  • 选第 \(c\) 种(越界食材):\(dp[i][j][k] += dp[i - 1][j - 1][k] * a_{i, c}\ (j > 0)\) \(O(1)\) 转移

  • 选其他食材:\(dp[i][j][k] += \sum_{u = 1, u != c}^{m}dp[i - 1][j][k - 1] * a_{i, u} (k > 0 )\)。$O(m) $ 转移

对答案的贡献

\(\sum f[n][j][k] (j > k)\)

优化:

  1. 跟之前一样可以滚动掉第一维

  2. 第三种转移最耗时,考虑用求解所有方案数量优化2的思想:\(\sum_{u = 1, u != c}^{m}dp[i - 1][j][k - 1] * a_{i, u} = dp[i - 1][j][k - 1] * (\sum_{u = 1, u != c}^{m}a_{i, u}) = dp[i - 1][j][k - 1] * (s_i - a_{i, c})\) 我们就做到了 \(O(1)\) 转移。

这步的时间复杂度

\(O(m)\) 枚举越界食材后,做一个 \(O(n ^ 3)\) 的 \(dp\)。求解不合法数量的总时间复杂度为 \(O(n ^ 3m)\)。

总时间复杂度:\(O(n ^ 3m)\)

  1. #include <cstdio>
  2. #include <iostream>
  3. #include <cstring>
  4. using namespace std;
  5. typedef long long LL;
  6. const int N = 105, M = 2005, P = 998244353;
  7. int n, m, a[N][M], f[N], s[N];
  8. int dp[N][N];
  9. /*
  10. dp[i][j] 表示不合法的选了 i 个,剩下的总共选了 j 个的方案数
  11. */
  12. LL ans = 0;
  13. /*
  14. f[i] 表示做了 i 道菜的方案数
  15. */
  16. void inline add(int &x, LL y) {
  17. x = (x + y) % P;
  18. }
  19. int main() {
  20. scanf("%d%d", &n, &m);
  21. for (int i = 1; i <= n; i++)
  22. for (int j = 1; j <= m; j++) {
  23. scanf("%d", &a[i][j]);
  24. s[i] = ((LL)s[i] + a[i][j]) % P;
  25. }
  26. f[0] = 1;
  27. for (int i = 1; i <= n; i++)
  28. for (int j = i; j; j--)
  29. add(f[j], (LL)f[j - 1] * s[i]);
  30. for (int i = 1; i <= n; i++) ans = (ans + f[i]) % P;
  31. for (int c = 1; c <= m; c++) {
  32. memset(dp, 0, sizeof dp);
  33. dp[0][0] = 1;
  34. for (int i = 1; i <= n; i++) {
  35. for (int j = i; ~j; j--) {
  36. for (int k = i - j; ~k; k--) {
  37. if(j) add(dp[j][k], (LL)dp[j - 1][k] * a[i][c]);
  38. if(k) add(dp[j][k], (LL)dp[j][k - 1] * (s[i] - a[i][c]));
  39. }
  40. }
  41. }
  42. for (int j = 1; j <= n; j++) {
  43. for (int k = 0; k < j; k++) ans = (ans - dp[j][k] + P) % P;
  44. }
  45. }
  46. printf("%lld\n", ans);
  47. return 0;
  48. }

100pts

延续 \(84pts\) 的思想,求解不合法数量的 \(O(n ^ 3m)\) 拖累了我们,我们考虑优化。

我们不关系具体越界食材与其他食材选了多少。只用保证越界食材数 $ > $ 其他食材数数即为不合法状态。


不妨把这两个的差作为一个维度,这样即可让 \(dp\) 状态降一维:

  • \(dp[i][j]\) 表示前 \(i\) 中烹饪方法,越界食材数 $ - $ 其他食材数 为 \(j\) 的方案数。

状态转移:

  • 第 \(i\) 种烹饪方法不选:\(dp[i][j] += dp[i - 1][j]\)
  • 选越界食材 \(c\):\(dp[i][j] += dp[i - 1][j - 1] * a_{i, c}\)
  • 选其他食材:\(dp[i][j] += dp[i - 1][j + 1] * (s_i - a_{i, c})\)。

答案贡献:

\(\sum dp[n][j] (j > 0)\)

总时间复杂度 \(O(n ^ 2m)\) 完美通过本题。

\(Tips\):

  1. 注意做差有可能为负数,我们可以把所有状态加一个 \(+n\) 的偏移量就不会数组越界了。

  2. 不要忘记取模!!

  1. #include <cstdio>
  2. #include <iostream>
  3. #include <cstring>
  4. using namespace std;
  5. typedef long long LL;
  6. const int N = 105, M = 2005, P = 998244353;
  7. int n, m, a[N][M], f[N], s[N];
  8. int dp[N][N << 1];
  9. /*
  10. dp[i][j] 表示不合法的选了 i 个,剩下的总共选了 j 个的方案数
  11. */
  12. LL ans = 0;
  13. /*
  14. f[i] 表示做了 i 道菜的方案数
  15. */
  16. void inline add(int &x, LL y) {
  17. x = (x + y) % P;
  18. }
  19. int main() {
  20. scanf("%d%d", &n, &m);
  21. for (int i = 1; i <= n; i++)
  22. for (int j = 1; j <= m; j++) {
  23. scanf("%d", &a[i][j]);
  24. s[i] = ((LL)s[i] + a[i][j]) % P;
  25. }
  26. f[0] = 1;
  27. for (int i = 1; i <= n; i++)
  28. for (int j = i; j; j--)
  29. add(f[j], (LL)f[j - 1] * s[i]);
  30. for (int i = 1; i <= n; i++) ans = (ans + f[i]) % P;
  31. for (int c = 1; c <= m; c++) {
  32. memset(dp, 0, sizeof dp);
  33. dp[0][n] = 1;
  34. for (int i = 1; i <= n; i++) {
  35. for (int j = 1; j <= n + i; j++) {
  36. dp[i][j] = (dp[i - 1][j] + (LL)dp[i - 1][j - 1] * a[i][c] + (LL)dp[i - 1][j + 1] * (s[i] - a[i][c])) % P;
  37. }
  38. }
  39. for (int j = n + 1; j <= n * 2; j++) ans = (ans - dp[n][j] + P) % P;
  40. }
  41. printf("%lld\n", ans);
  42. return 0;
  43. }

CSP-S 2019 Emiya 家今天的饭的更多相关文章

  1. 洛谷P5664 Emiya 家今天的饭 问题分析

    首先来看一道我编的题: 安娜写宋词 题目背景 洛谷P5664 Emiya 家今天的饭[民间数据] 的简化版本. 题目描述 安娜准备去参加宋词大赛,她一共掌握 \(n\) 个 词牌名 ,并且她的宋词总共 ...

  2. 洛谷P5664 Emiya 家今天的饭 题解 动态规划

    首先来看一道题题: 安娜写宋词 题目背景 洛谷P5664 Emiya 家今天的饭[民间数据] 的简化版本. 题目描述 安娜准备去参加宋词大赛,她一共掌握 \(n\) 个 词牌名 ,并且她的宋词总共有 ...

  3. 【CSP-S 2019】【洛谷P5664】Emiya 家今天的饭【dp】

    题目 题目链接:https://www.luogu.org/problem/P5664 Emiya 是个擅长做菜的高中生,他共掌握 \(n\) 种烹饪方法,且会使用 \(m\) 种主要食材做菜.为了方 ...

  4. 【CSP-S 2019】D2T1 Emiya 家今天的饭

    Description 传送门 Solution 算法1 32pts 爆搜,复杂度\(O((m+1)^n)\) 算法2 84pts 裸的dp,复杂度\(O(n^3m)\) 首先有一个显然的性质要知道: ...

  5. Emiya家今天的饭 NOIP2019 (CSP?) 类DP好题 luoguP5664

    luogu题目传送门! 首先,硬求可行方案数并不现实,因为不好求(去年考场就这么挂的,虽然那时候比现在更蒟). 在硬搞可行方案数不行之后,对题目要求的目标进行转换: 可行方案数 = 总方案数 - 不合 ...

  6. 「CSP-S 2019」Emiya 家今天的饭

    description loj 3211 solution 看到题目中要求每种主要食材至多在一半的菜中被使用,容易想到补集转换. 即\(ans=\)总方案数-存在某一种食材在一半以上的菜中被使用的方案 ...

  7. [CSP-S 2019 Day2]Emiya家今天的饭

    思路: 这种题目就考我们首先想到一个性质.这题其实容易想到:超限的菜最多只有一个,再加上这题有容斥那味,就枚举超限的菜然后dp就做完了. 推式子能力还是不行,要看题解. 式子还需要一个优化,就是废除冗 ...

  8. [CSP-S 2019 day2 T1] Emiya家今天的饭

    题面 题解 不考虑每种食材不超过一半的限制,答案是 减去 1 是去掉一道菜都不做的方案. 显然只可能有一种菜超过一半,于是枚举这种菜,对每个方式做背包即可(记一维状态表示这种菜比别的菜多做了多少份). ...

  9. [CSP-S2019]Emiya 家今天的饭 题解

    CSP-S2 2019 D2T1 很不错的一题DP,通过这道题学到了很多. 身为一个对DP一窍不通的蒟蒻,在考场上还挣扎了1h来推式子,居然还有几次几乎推出正解,然而最后还是只能打个32分的暴搜滚粗 ...

随机推荐

  1. python实现二叉树递归遍历与非递归遍历

    一.中序遍历 前中后序三种遍历方法对于左右结点的遍历顺序都是一样的(先左后右),唯一不同的就是根节点的出现位置.对于中序遍历来说,根结点的遍历位置在中间. 所以中序遍历的顺序:左中右 1.1 递归实现 ...

  2. day93:flask:

    目录 1.HTTP的会话控制 2.Cookie 3.Session 4.请求钩子 5.捕获错误 6.上下文:context 7.Flask-Script 1.HTTP的会话控制 1.什么是会话控制? ...

  3. Linux文件监控工具——inotify-tools

    举例: ip.txt内容如下: 10.1.1.11 root 123 10.1.1.22 root 111 10.1.1.33 root 123456 10.1.1.44 root 54321 写法1 ...

  4. No matching distribution found for Tensorflow

    No matching distribution found for Tensorflow 原因:python 3.7.3 版本过高 解决:安装3.6  64位

  5. 了解 MySQL的数据行、行溢出机制吗?

    目录 一.行 有哪些格式? 二.紧凑的行格式长啥样? 三.MySQL单行能存多大体量的数据? 四.Compact格式是如何做到紧凑的? 五.什么是行溢出? 六.行 如何溢出? 七.思考一个问题 关注送 ...

  6. php form表单提交时,action url中参数无效的解决方法

    表单提交时get方式的一个错误 <form class="form-inline pull-right" method="get" action=&quo ...

  7. Java基础教程——解析注解

    解析注解 Java 5开始,java.lang.reflect包下的反射API可以在运行时读取Annotation. 应用:定义一个自动执行方法的注解,解析注解.通过反射执行方法,替代配置文件. pa ...

  8. Java基础教程——运算符

    运算符 算术运算符 加 减 乘 除 取余 自加 自减 + - * / % ++ -- public class 算术运算符 { public static void main(String[] arg ...

  9. 关于 spring security 对用户名和密码的校验过程

    1.执行 AuthenticationManager 认证方法 authenticate(UsernamePasswordAuthenticationToken) 2.ProviderManager ...

  10. 学习abp vnext框架到精简到我的Vop框架

    学习目标 框架特点 基于.NET 5平台开发 模块化系统 极少依赖 极易扩展 ....... 框架目的 学习.NET 5平台 学习abp vnext 上图大部分功能已经实现,多数是参考(copy)ab ...