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的代码。

 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 }
View Code

 

 

然后我们再考虑优化。因为这题是计数问题,所以不能用多重背包的单调队列优化。但是这题的物品都是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]

 

posted @ 2014-08-15 14:58  james47  阅读(521)  评论(0编辑  收藏  举报