2287: 【POJ Challenge】消失之物
Description
ftiasch 有 N 个物品, 体积分别是 W1, W2, ..., WN。 由于她的疏忽, 第 i 个物品丢失了。 “要使用剩下的 N - 1 物品装满容积为 x 的背包,有几种方法呢?” -- 这是经典的问题了。她把答案记为 Count(i, x) ,想要得到所有1 <= i <= N, 1 <= x <= M的 Count(i, x) 表格。
Input
第1行:两个整数 N (1 ≤ N ≤ 2 × 103) 和 M (1 ≤ M ≤ 2 × 103),物品的数量和最大的容积。
第2行: N 个整数 W1, W2, ..., WN, 物品的体积。
Output
一个 N × M 的矩阵, Count(i, x)的末位数字。
Sample Input
1 1 2
Sample Output
11
21
HINT
如果物品3丢失的话,只有一种方法装满容量是2的背包,即选择物品1和物品2。
如果说这是一道水题,不错。但如果说这仅仅是一道水题,那我会很生气。
背包问题当然很经典。f[i][j]表示前i个物品恰好装满容量为j的背包的方案数。显然可得:
初始状态:f[0][0]=1,f[0][j]=0(j≠0)
状态转移:f[i][j]+=f[i-1][j-w[i]](j>w[i])
然后我们继续。要算出c[i][j]表示除去第i个物品后剩下n-1个物品恰好装满容量为j的背包的方案数。亦可得:
c[i][0]=1
c[i][j]=f[n][j] (j<w[i]) 显然i号物品装不下
c[i][j]=f[n][j]-c[i][j-w[i]](j>=w[i]) f[n][j]是用了n种物品,我们要减去使用了i的方案数。而使用了i的方案数=c[i][j-w[i]]
当然,我们可以把空间降维,于是空间O(m),时间O(nm)。
但是!!!
我们为什么一定要这样做?
法2:网上有一神犇算出了前缀与后缀的方案数(上述的f即为前缀方案数)。然后对于每一个不能取的i,他对i-1的前缀与i+1的后缀做了值域卷积FFT。空间O(nm),时间O(nmlog m)。
法3:上面的做法其实十分暴力。但是,我们可以使用一些不一样的做法来实现法2的思想。
这个方法很值得推广,叫做“分治背包”。
先想更彻底的暴力。对于每一个物品,我们都需要算出除它以外物品叠加产生的答案。这样的时间复杂度是O(n*n*m)。
但是,我们可以发现:中间有很多统计是重复的。甚至说,是大多数也不为过。如果可以削去一些重复运算,那么效率会大大提高。
而现在,我们试图算出1~n这个整体中每一个数的ans,以此减少重复运算,我们可以使用二分。可以用类似于“线段树”的画风来理解这个问题,最开始我们在1~n这条线段上,它在DFS树上有2个儿子,一个是1~mid,另一个是mid+1~rg。这样递归下去,最后lf==rg,我们试图在此时输出i=lf的答案。
我们已经说过,对于一个数i,我们需要把1~i-1与i+1~n号物品的影响都叠加一遍。而这可以使用DFS回溯解决。现在需要算出lf~rg的答案。可知,mid+1~rg号物品的影响一定会被叠入lf~mid中的每一个物品中。
话不多说了,直接上代码(多读几遍就懂了):
1 /************************************************************** 2 Problem: 2287 3 User: Doggu 4 Language: C++ 5 Result: Accepted 6 Time:368 ms 7 Memory:924 kb 8 ****************************************************************/ 9 10 #include <cstdio> 11 const int S = 12; 12 const int N = 2010; 13 const int M = 2010; 14 int n, m, w[N], dp[S][M]; 15 void solve(int dep,int lf,int rg) { 16 if(lf==rg) { 17 for( int j = 1; j <= m; j++ ) printf("%d",dp[dep][j]); 18 printf("\n"); 19 return ; 20 } 21 int mid=(lf+rg)>>1; 22 for( int j = 0; j <= m; j++ ) dp[dep+1][j]=dp[dep][j]; 23 for( int i = mid+1; i <= rg; i++ ) 24 for( int j = m; j >= w[i]; j-- ) 25 dp[dep+1][j]+=dp[dep+1][j-w[i]], dp[dep+1][j]%=10; 26 solve(dep+1,lf,mid); 27 for( int j = 0; j <= m; j++ ) dp[dep+1][j]=dp[dep][j]; 28 for( int i = lf; i <= mid; i++ ) 29 for( int j = m; j >= w[i]; j-- ) 30 dp[dep+1][j]+=dp[dep+1][j-w[i]], dp[dep+1][j]%=10; 31 solve(dep+1,mid+1,rg); 32 } 33 int main() { 34 scanf("%d%d",&n,&m); 35 for( int i = 1; i <= n; i++ ) scanf("%d",&w[i]); 36 dp[0][0]=1; 37 solve(0,1,n); 38 return 0; 39 } 40
如果你看懂了,那我们就可以继续了。因为,背包问题是很容易插入新物品的,且插入的顺序对最后结果不会产生影响。而一般情况下,我们会统计前缀。在分治中,我们的顺序变了,但是效果是完全一样的。请注意,对于“整体DP以去重”这一方法,该条件极其必要的。
最后说一下,如果用分治背包,空间是O(mlog n)的,时间是O(nmlog n)的。
如果很懵,或是已经听懂了,可以再看一看:旧题新做:FLOJ Round #19 A 最短路
附录·速度比较:
分治背包 | 924 kb | 368 ms | 889 B |
法1压缩空间 | 844 kb | 356 ms | 546 B |
法1平方空间 | 16944 kb | 364 ms | 523 B |