hdoj4906 Our happy ending(2014 Multi-University Training Contest 4)
对于一个定长(size = n)的数列a, 若其存在“位置相关”的子集(含空集)使得该子集所有元素之和为k,那么将数列a计数。
其中数列a中任一元素a[i]在[0, l]内自由取值。
数据条件0≤n, k ≤ 20, 0≤ l ≤ 1e9,计数结果对mod = 1e9 + 7取模。
无论直接计数还是考虑从反面计数都解决不了去重的问题,只能考虑dp。
枚举数列的长度i和压缩后的状态j,并且记录在该条件下的数列选取方案数dp[i][j]。
压缩后的状态j表示对于集合{1, 2, ..., min(l, k)}的选取情况。
其中集合中第i个元素在状态j中当且仅当j的二进制串的第i位为1。
显然我们有dp[0][0] = 1。
对于长度为p的数组,第p位可以选取的元素是0,1,2,...,l
考虑p位选取1,2,...,min(l, k)
那么对于数组长度为p-1时的任一状态j,在数组后追加元素i后的状态为:
j1 = j | ((j << i) & ((1 << k) - 1)) | (1 << (i - 1))
三部分分别表示原状态,原状态每个元素与元素i求和后增加的状态,i本身。
于是dp[p][j1] += dp[p - 1][j]。
而对于p位选取0或者大于min(l, k)的情形,j1 = j。
因此有dp[p][j] = (l - min(l, k)) * dp[p - 1][j]。
我们最后只需对dp[n][j],其中j第k位为1的累加即可。
由于数列每个元素非负,我们最后只关心那些集合存在和为k的子集,
而那些大于k的元素必然不是构成子集的元素,只有那些小于k的元素才对状态转移有用。
因此我们可以这样表示状态。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <map> 5 #include <string> 6 #include <vector> 7 #include <set> 8 #include <cmath> 9 #include <ctime> 10 using namespace std; 11 #define lson (u << 1) 12 #define rson (u << 1 | 1) 13 typedef __int64 ll; 14 const int maxn = 1e6 + 10; 15 const int maxm = 1050; 16 const ll mod = 1e9 + 7; 17 18 int n, l, k; 19 ll dp[(1 << 21) + 10]; 20 int main(){ 21 int T; 22 scanf("%d", &T); 23 while(T--){ 24 scanf("%d%d%d", &n, &k, &l); 25 ll d = abs(l - k); 26 l = min(l, k); 27 int s = (1 << k) - 1; 28 memset(dp, 0, sizeof dp); 29 dp[0] = 1; 30 while(n--){ 31 for(int j = s; j >= 0; j--){ 32 ll tem = dp[j]; 33 if(!tem) continue; 34 for(int p = 1; p <= l; p++){ 35 int nex = (1 << (p - 1)) | j | ((j << p) & s); 36 dp[nex] = (dp[nex] + tem) % mod; 37 } 38 dp[j] = (tem * (1 + d)) % mod; 39 } 40 } 41 ll ans = 0; 42 for(int i = s; i >= 0; i--) 43 if(i & (1 << (k - 1))) 44 ans = (ans + dp[i]) % mod; 45 printf("%I64d\n",ans); 46 } 47 return 0; 48 }