对于给定的两个约束条件,可以通过联立方程组直接解出子序列A的和和子序列B的和,即sum(A) = (r + s) / 2,sum(B) = (r - s) / 2,假设|A|=|B|=n
所以问题变成了,在一个数组中求长度为n且子序列和为sum(A)或sum(B)有多少个。
假设count(n, s)表示长度为n且子序列和为s有多少个,则要求的是count(n, sum(A)) * count(n, sum(B)),其中1<=n<=m
或者通俗来说就是m个k-sum问题
求k-sum,如果用搜索的方法,时间复杂度大概是n^k量级(可以优化到n^(k - 1) + nlogn),何况现在要求的是m个k-sum问题,即使排序+剪枝优化肯定也会超时(别问我是怎么知道的)
所以只能选择动归,因为每次求k-sum的过程存在大量重复计算。
令f[i][j][k]表示从第i个元素开始,子序列长度为j,子序列和为k,这样的子序列有多少个
那么有地推公式:f[i][j][k] = f[i+1][j - 1][k - a[i]] + f[i + 1][j][k]
这个公式其实挺straightforward,跟背包问题是一样的。
由于sum(A)和sum(B)最大不过2000,所以k的范围是2000,另外i和j的范围分别是m的范围100,所以如果不做任何状态压缩,占用空间大概是2000*100*100*4*8约为640MB(用int存储),显然要爆。所以还必须状态压缩。
分析地推公式,明显压缩i那维,而且不难看出j和k必须从后向前推。还是跟背包问题一样。
代码:
1 #include <cmath> 2 #include <cstdio> 3 #include <vector> 4 #include <iostream> 5 #include <algorithm> 6 #include <cstring> 7 using namespace std; 8 9 #define MAX_SIZE 128 10 #define MAX_SUM 2048 11 #define MOD 1000000007 12 13 long long a[MAX_SIZE]; 14 int m, r, s; 15 long long f[MAX_SIZE][MAX_SUM]; 16 17 int main() { 18 /* Enter your code here. Read input from STDIN. Print output to STDOUT */ 19 int sa, sb; 20 long long res = 0; 21 22 cin >> m >> r >> s; 23 sa = (r + s) / 2; 24 sb = (r - s) / 2; 25 for (int i = 0; i < m; i++) 26 cin >> a[i]; 27 28 memset(f, 0, sizeof(f)); 29 f[0][0] = 1; 30 for (int i = m - 1; i >= 0; i--) { 31 for (int j = m; j >= 1; j--) { 32 for (int k = 2000; k >= 0; k--) { 33 if (k >= a[i]) 34 f[j][k] = (f[j][k] + f[j - 1][k - a[i]]) % MOD; 35 } 36 } 37 } 38 39 for (int i = 1; i <= m; i++) 40 res = (res + (f[i][sa] * f[i][sb]) % MOD) % MOD; 41 42 cout << res << endl; 43 return 0; 44 }
第一次用的int,结果提交以后有些case是WA,考虑到DP问题基本上不会出现WA的情况,所以断定应该是数据溢出了,换成long long果然就没问题了。