https://www.luogu.com.cn/problem/P5664
題意解讀:
Emiya 掌握 n 種烹飪方法和 m 種主要食材,用第 i 種烹飪方法和第 j 種食材可做 a[i][j]
- 每道菜的烹飪方法互不相同(即每種烹飪方法最多選 1 道菜);
- 每種主要食材的使用次數不超過總菜數 k 的一半(即 ≤ k / 2)。
解題思路:
1、容斥思想
對於每一種烹飪方法i,一共可以做出s[i] = a[i][1] + a[i][2] + ... + a[i][m]種菜。
根據約束,每一種烹飪方法只能選一種菜,但是還有一個約束是同一食材的菜不能超過總菜數的一半,這就不好直接選。
在第一個條件滿足,第二個條件不滿足的某一種不合法方案中,只可能有一個食材的菜數量超過總菜數的一半(如果有兩個食材超過,總菜數就>k)。
因此,可以用容斥原理來計算:所有選菜的方案數 - 所有同一食材的菜超過半數的方案數
2、乘法原理計算所有選菜的方案數
當不考慮第二個約束條件,所有選菜的方案數就是在每一種烹飪方法裏選,
每一種烹飪方法都有s[i] + 1中選法(含不選該烹飪方法的任何菜),注意至少要選一個菜,因此所有選菜方案數為:
(s[1]+1) * (s[2]+1) * ... * (s[n]+1) - 1 (減一是排除一個菜都不選的情況)
3、動態規劃計算所有同一食材的菜超過半數的方案數
由於只會有一種食材的菜超量,不妨枚舉超量的食材p
設f[i][j][t]表示前i種烹飪方式,一共選了j道菜,包含食材p的菜數量為t的方案數
根據定義,對於食材p,非法的方案數為所有t>j/2時的f[i][j][t]之和
狀態轉移方程為:
第i種烹飪方式一個菜都不選:f[i][j][t] = f[i-1][j][t]
第i種烹飪方式選非食材p的菜:f[i][j][t] = f[i-1][j-1][t] * (s[i] - a[i][p])
第i種烹飪方式選食材p的菜:f[i][j][t] = f[i-1][j-1][t-1] * a[i][p]
分析一下時間複雜度:
枚舉所有的食材m,再枚舉所有烹飪方式n,再枚舉所有菜的數量n,再枚舉某個食材的菜的數量n,總體為O(mn3),不可行。
4、差值替換優化
f[i][j][t]中j和t存在的目的是為了比較t>j/2,不妨用一個d = t - (j - t)的差值來表示,也就是對於某個食材p,
f[i][d]表示前i個烹飪方式中,選了p食材的菜與沒有選p食材的菜的數量的差值為d的方案數,
這樣所有d>0的f[i][d]都是非法的方案,
由於d的取值範圍變成了[-n, n],可以加上n值進行修正,避免負數。
狀態轉移方程為(實際編程時注意d要加上修正值):
第i種烹飪方式一個菜都不選:f[i][d] = f[i-1][d]
第i種烹飪方式選非食材p的菜:f[i][d] = f[i-1][d+1] * (s[i] - a[i][p])
第i種烹飪方式選食材p的菜:f[i][d] = f[i-1][d-1] * a[i][p]
f[i][d]取三種情況之和。
分析一下時間複雜度:
枚舉所有的食材m,再枚舉所有烹飪方式n,再枚舉所有選某個食材的菜的數量與沒選的差值2n,總體為O(mn2)。
100分代碼:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 105, M = 2005, MOD = 998244353, OFFSET = 100;
LL s[N], a[N][M], f[N][2 * N];
LL n, m, ans;
//計算不考慮第二個約束條件,所有選菜的方案數
LL total()
{
LL res = 1;
for(int i = 1; i <= n; i++)
res = res * (s[i] + 1) % MOD;
return res - 1;
}
//計算違反第二個約束條件的選菜方案數
LL illegal()
{
LL res = 0;
for(int p = 1; p <= m; p++) //枚舉不合法的食材
{
memset(f, 0, sizeof f);
f[0][OFFSET] = 1; //初始狀態
for(int i = 1; i <= n; i++) //枚舉烹飪方式
{
for(int d = -n; d <= n; d++) //枚舉選食材p的菜的數量與不選食材p的菜的數量之差
{
//第i種烹飪方式一個菜都不選
f[i][d + OFFSET] = (f[i][d + OFFSET] + f[i - 1][d + OFFSET]) % MOD;
//第i種烹飪方式選非食材p的菜
if(d + 1 + OFFSET >= 0 && d + 1 + OFFSET < 2 * N)
f[i][d + OFFSET] = (f[i][d + OFFSET] + f[i - 1][d + 1 + OFFSET] * (s[i] - a[i][p])) % MOD;
//第i種烹飪方式選食材p的菜
if(d - 1 + OFFSET >= 0 && d - 1 + OFFSET < 2 * N)
f[i][d + OFFSET] = (f[i][d + OFFSET] + f[i - 1][d - 1 + OFFSET] * a[i][p]) % MOD;
}
}
for(int d = 1; d <= n; d++) //統計不合法方案數
res = (res + f[n][d + OFFSET]) % MOD;
}
return res;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> a[i][j];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
s[i] = (s[i] + a[i][j]) % MOD;
ans = total() - illegal();
ans = (ans % MOD + MOD) % MOD;
cout << ans << endl;
return 0;
}