P4996 咕咕咕
阿新 • • 發佈:2018-11-06
貢獻法+組合數學
講道理一看到辣麼小的\(n\),我就想到了狀壓dp。
列舉子集?哦,\(O(3^n)\)的複雜度。但是我沒有注意到\(n=20\)的時候是過不了的。
接下來我就在想70pts的狀壓dp要怎麼寫?
沒有清楚定義狀態的我連樣例2都過不了,只能夠手動列舉拿30pts。
所以最簡單的月賽也爆炸了
70pts的狀壓
下面的做法是借鑑luogu題解的,不是抄襲。(你信嗎?)
設dp[i]
表示從全0轉移到i狀態的所有歉意值之和。
那麼會有
\[dp[i]=\sum{dp[j] + qy[j] \times num[j]}, j \in i and j \neq \emptyset\]
其中num[j]
表示從全0轉移到j的方案數,qy[j]
表示當前這個狀態的歉意值。
num[j]
自然也是可以遞推的,可以這麼遞推:
\[num[j] = \sum{num[k]}, k \in j and k \neq \emptyset\]
用列舉子集的優化技巧就可以做到\(O(3^n)\)的搞出來了。
不給程式碼,題解裡面是有的。
我的錯誤之處:沒有意識到方案數是對答案有影響的。
滿分做法
使用貢獻法,那麼每一個歉意值就會對答案產生貢獻了。
產生的貢獻是多少?是這個狀態在所有方案中出現的次數 乘以 這個方案的歉意值。
還能夠發現:其實一個狀態只跟它的01個數有關,跟她們的順序是無關的。
所以設一個\(num[i]\)表示填\(i\)個1的方案數,那麼就可以遞推出來了。
遞推式是這樣子的:
\[num[i] = \sum_{j=1}^n{num[i-j] \times C_i^j}\]
那麼最後的答案就是該方案0的個數 乘以 該方案1的個數 乘以 這個方案的歉意值。
對了:注意取膜!否則你就只有80pts啦。
程式碼:
#include<cstdio> #include<cstring> #define ll long long const int maxn = 21, maxN = 1048580; const int MOD = 998244353; ll dp[maxN]; ll qy[maxN]; ll num[maxn]; ll C[maxn][maxn]; int n, m; void init() { for(int i = 0; i <= 20; i++) { C[i][0] = C[i][i] = 1; } for(int i = 1; i <= 20; i++) { for(int j = 1; j < i; j++) { C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD; } } } int popcount(char *ch) { int len = strlen(ch); int res = 0; for(int i = len - 1; i >= 0; i--) { if(ch[i] == '1') res++; } return res; } int main() { init(); scanf("%d%d", &n, &m); num[0] = 1; for(int i = 1; i <= n; i++) { for(int j = 1; j <= i; j++) { num[i] = (num[i] + C[i][i - j] * num[i - j] % MOD) % MOD; } } ll ans = 0; for(int i = 1; i <= m; i++) { char ch[25]; int x; scanf("%s%d", ch, &x); int temp = popcount(ch), len = strlen(ch); ans = (ans + x * num[temp] % MOD * num[len - temp] % MOD) % MOD; } printf("%lld\n", ans); return 0; }