HDOJ 4945 2048 DP计数
http://acm.hdu.edu.cn/showproblem.php?pid=4945
题意:类似于2048游戏,给你一个10万个数的数列,每个数在0-2048之间。对于该数列的子序列(集合),可以任选两个相同的数合并,如果最后能合并出一个2048,那么这个序列合法。问有多少个合法的子序列。
分析:
因为不能合并不同的数,所以先排除不是2的幂次的数,记为cnt,最后方案数乘上2^cnt即可。稍加思考可以得到,集合合法,要求里面2^x的数的和至少2048。直接求比较困难,我们反着考虑,求少于2048的集合个数。可以发现其实就是一个多重背包的模型。
dp[i][j]表示用从2^0,2^1..到2^i组成j的方案数,则dp[i][j] = Σ(dp[i-1][j-k*(2^i)]*C(k,count[2^i]))。最后小于2048的方案数就是从0累加到2047,用总的方案数减去它,乘上上面所说的2^cnt即可。状态是11*2048,转移最多2048,总共最多不会超过11*2048*2048。
然而这题比较坑爹,这种复杂度本应该是可以通过的,但是写得不够好就会超时。下面贴一份1.2s的代码。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 const int mod = 998244353; 7 int n, cnt[2050]; 8 long long dp[2050], inv[2050], c[15][2050]; 9 10 int read() 11 { 12 int ans = 0; 13 char ch = getchar(); 14 while(ch < '0' || ch > '9') ch = getchar(); 15 while(ch >= '0' && ch <= '9'){ 16 ans = ans * 10 + ch - '0'; 17 ch = getchar(); 18 } 19 return ans; 20 } 21 void init() 22 { 23 inv[1] = 1; 24 for (int i = 2; i <= 2048; i++) inv[i] = inv[mod%i] * (mod - mod/i) % mod; 25 } 26 void getcom(int i, int n) 27 { 28 int lim = min(n, 2048/(1<<i)); 29 c[i][0] = 1; 30 for (int j = 1; j <= lim; j++) 31 c[i][j] = c[i][j-1] * (n-j+1) %mod * inv[j] %mod; 32 } 33 long long powmod(long long a, long long n, long long mod) 34 { 35 if (n == 0) return 1; 36 if (n == 1) return a; 37 if (n & 1) return a * powmod(a * a % mod, n/2, mod) % mod; 38 return powmod(a * a % mod, n/2, mod); 39 } 40 int main() 41 { 42 int cas = 0; 43 init(); 44 while(scanf("%d", &n)) 45 { 46 if (n == 0) break; 47 for (int i = 1; i <= 2048; i <<= 1) cnt[i] = 0; 48 for (int i = 0; i < n; i++) cnt[read()] ++; 49 int tot = 0; 50 for (int i = 0; (1 << i) <= 2048; i++){ 51 getcom(i, cnt[1<<i]); 52 tot += cnt[1<<i]; 53 } 54 memset(dp, 0, sizeof(dp)); 55 dp[0] = 1; 56 for (int i = 0; (1 << i) <= 2048; i++){ 57 int w = 1 << i; 58 for (int j = 2047-w; j >= 0; j--){ 59 if (dp[j] == 0) continue; 60 for (int k = 1; k <= cnt[1<<i]; k++){ 61 if (j + k * w > 2047) break; 62 dp[j+k*w] = (dp[j+k*w] + dp[j] * c[i][k] % mod) % mod; 63 } 64 } 65 } 66 int anst = 0; 67 for (int i = 0; i < 2048; i++) anst = anst + dp[i], anst = anst >= mod? anst-mod: anst; 68 int ans = (powmod(2, tot, mod) - anst + mod) % mod * powmod(2, n-tot, mod) % mod; 69 printf("Case #%d: %d\n", ++cas, ans); 70 } 71 return 0; 72 }
然后我们再考虑优化。因为这题是计数问题,所以不能用多重背包的单调队列优化。但是这题的物品都是2^x,我们可以利用这个特性。考虑当前枚举到512,那么实际上0..511都是等价的,一个512加上他们都没法得到1024。同理,512..1023是等价的。。。回头,对于2,0和1是等价的,2和3是等价的,4和5是等价的。。于是我们可以把这些数都压缩到一个状态里,加速转移。加上这个优化和读入优化,用c++交,可以到300ms。
关键部分:
1 void zip(int x) 2 { 3 for (int i = 0; i+x <= 2048; i += (x<<1)) 4 dp[i] = (dp[i] + dp[i+x]) % mod; 5 } 6 7 for (int i = 0; (1<<i) <= 2048; i++){ 8 int w = 1 << i; 9 for (int j = 2048; j >= 0; j -= w){ 10 if (dp[j] == 0) continue; 11 for (int k = 1; k <= cnt[w]; k++){ 12 if (j + k * w > 2047) break; 13 dp[j+k*w] = (dp[j+k*w] + dp[j] * c[i][k] % mod) % mod; 14 } 15 } 16 zip(w); 17 }
//最后dp[0]等同于上面的dp[0]+..+dp[2047]