USACO是USA Computing Olympiad的简称,它组织了很多面向全球的计算机竞赛活动。
USACO Trainng是一个很适合初学者的题库,我认为它的特色是题目质量高,循序渐进,还配有不错的课文和题目分析。其中关于背包问题的那篇课文 (TEXT Knapsack Problems) 也值得一看。
另外,USACO Contest是USACO常年组织的面向全球的竞赛系列,在此也推荐NOIP选手参加。
我整理了USACO Training中涉及背包问题的题目,应该可以作为不错的习题。其中标加号的是我比较推荐的,标叹号的是我认为对NOIP选手比较有挑战性的。
题目列表
题目简解
以下文字来自我所撰的《USACO心得》一文,该文的完整版本,包括我的程序,可在DD的USACO征程中找到。
Inflate 是加权01 背包问题,也就是说:每种物品只有一件,只可以选择放或者不放;而且每种物品有对应的权值,目标是使总权值最大或最小。它最朴素的状态转移方程是:f[k][i] = max{f[k-1][i] , f[k-1][i-v[k]]+w[k]}。f[k][i]表示前k 件物品花费代价i 可以得到的最大权值。v[k]和w[k]分别是第k 件物品的花费和权值。可以看到, f[k]的求解过程就是使用第k 件物品对f[k-1]进行更新的过程。那么事实上就不用使用二维数组,只需要定义f[i],然后对于每件物品k,顺序地检查f[i]与f[i-v[k]]+w[k]的大小,如果后者更大,就对前者进行更新。这是背包问题中典型的优化方法。
题目stamps 中,每种物品的使用量没有直接限制,但使用物品的总量有限制。求第一个不能用这有限个物品组成的背包的大小。(可以这样等价地认为)设f[k][i] 表示前k 件物品组成大小为i 的背包, 最少需要物品的数量。则f[k][i]= min{f[k-1][i],f[k-1][i-j*s[k]]+j},其中j 是选择使用第k 件物品的数目,这个方程运用时可以用和上面一样的方法处理成一维的。求解时先设置一个粗糙的循环上限,即最大的物品乘最多物品数。
Money 是多重背包问题。也就是每个物品可以使用无限多次。要求解的是构成一种背包的不同方案总数。基本上就是把一般的多重背包的方程中的min 改成sum 就行了。
Nuggets 的模型也是多重背包。要求求解所给的物品不能恰好放入的背包大小的最大值(可能不存在)。只需要根据“若i、j 互质,则关于x、y 的不定方程i*x+y*j=n 必有正整数解,其中n>i*j”这一定理得出一个循环的上限。 Subsets 子集和问题相当于物品大小是前N 个自然数时求大小为N*(N+1)/4 的 01 背包的方案数。
Rockers 可以利用求解背包问题的思想设计解法。我的状态转移方程如下: f[i][j][t]=max{f[i][j][t-1] , f[i-1][j][t] , f[i-1][j][t-time[i]]+1 , f[i-1][j-1][T]+(t>=time[i])}。其中 f[i][j][t]表示前i 首歌用j 张完整的盘和一张录了t 分钟的盘可以放入的最多歌数,T 是一张光盘的最大容量,t>=time[i]是一个bool 值转换成int 取值为0 或1。但我后来发现我当时设计的状态和方程效率有点低,如果换成这样:f[i][j]=(a,b)表示前i 首歌中选了j 首需要用到a 张完整的光盘以及一张录了b 分钟的光盘,会将时空复杂度都大大降低。这种将状态的值设为二维的方法值得注意。
Milk4 是这些类背包问题中难度最大的一道了。很多人无法做到将它用纯DP 方法求解,而是用迭代加深搜索枚举使用的桶,将其转换成多重背包问题再DP。由于 USACO 的数据弱,迭代加深的深度很小,这样也可以AC,但我们还是可以用纯DP 方法将它完美解决的。设f[k]为称量出k 单位牛奶需要的最少的桶数。那么可以用类似多重背包的方法对f 数组反复更新以求得最小值。然而困难在于如何输出字典序最小的方案。我们可以对每个i 记录pre_f[i]和pre_v[i]。表示得到i 单位牛奶的过程是用pre_f[i]单位牛奶加上若干个编号为pre_v[i]的桶的牛奶。这样就可以一步步求得得到i 单位牛奶的完整方案。为了使方案的字典序最小,我们在每次找到一个耗费桶数相同的方案时对已储存的方案和新方案进行比较再决定是否更新方案。为了使这种比较快捷,在使用各种大小的桶对f 数组进行更新时先大后小地进行。USACO 的官方题解正是这一思路。如果认为以上文字比较难理解可以阅读官方程序或我的程序。
Copyright (c) 2007 Tianyi Cui
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation.
整理 by stntwm
完全背包问题优化分析
无限背包,最容易想到的自然是(所谓无限背包,意思是说,物品可以无限地选取,而不是只能选择一个)
(1)定义F(i,j)为i时间前j个物品的最优解可知
F(i,j)=max{ F(k,j-1)+(i -k ) div t[j]*w[j] } (0<=k<=i)
此种算法复杂度接近O(n^3),很不理想,考虑到可以不枚举分配的时间而是枚举
选择多少个j物品,可以得到一个较上式为优化的方程.
F(i,j)=max{ F(i-t[j]*k,j-1)+k*w[j] } (0<=k<=i mod t[j] );
这样得到进一步优化.但是这样依然要TLE第二个数据.
另外一个问题是直接开数据将要MLE.对于这类方程,我们用的最多的办法,最容易理解的办法就是滚动数据,滚动数组可以用mod来实现,也可以开两个一维数组变量之后交替复制,就是在每次递归之前多加一句g:=f;其中F是当前数组,G是上一数组.
但事实上,可以只用一个一维数组,关键在于递推的顺序性,一般是这样实现的.!!下面的代码没有经过debug或者make,而且仅仅是一般的背包问题的伪代码1 for(int j=1;j<=n;j++)
2 for(int i=m;i>=0;i--)
3 if ((t[j]<=i)&&(f[i-t[j]]+w[j]>f[i]))
4 f[i]=f[i-t[j]]+w[j];
结论:在每进行一次第一行的循环节(就是2-4行)后,数组f中f就是前j个物品在i时间时的最优解.为什么?当代码尚未开始执行,f显然满足这样的性质,每次循环时我们用第j个物品尝试加入到原来i时间的方案中,如果发现当前解更优,就更新f中的值.要注意其中i的循还顺序,当解问题f(i,j)时,我们需要问题f(k,j-1)的解,其中k<=i,换言之,在上述代码中,我们解决f这一子问题的时候需要保证f[k](k<=i)的数据还是未更新过的数据.如果把i的循环顺序掉转,会得到怎么样的结果呢?下面有分析.
(在这样的实现方案中,第二行的i可以简单地至t[j]停止循环)
由以上的思路,对于这道题目,我们得到了这样的代码
1 for j:=1 to n do
2 for i:=m downto t[j] do
3 tmp1:=i;
4 tmp2:=0;
5 for k:=1 to i div t[j] do
6 dec(tmp1,t[j]);
7 inc(tmp2,w[j]);
8 tmp:=f[tmp1]+tmp2;
9 if tmp>f then f:=tmp;
但是即使是这样的代码,在第二个数据的面前还是要TLE.....
最后我们发现,对于无限背包,每个物品可以取到无数次,换言之,我们只希望最后解最优,而不考虑是否解里面有多个相同的物品.也就是说,我们可以把第j个物品加到已经加过第J物品的解上.别的分析和一般的01背包是一致的,关键在于如何反复地检验能否加入第J个物品.
回想一般的01背包的代码,我们在代码第2行让i从m循环到t[j],以此来保证第J个物品将会加到一个尚没有出现过该物品的方案中,那么,如果我们反过来循环呢?
1 for j:=1 to n do
2 for i:=t[j] to m do
3 if (t[j]<=i)and(f[i-t[j]]+w[j]>f then
4 f:=f[i-t[j]]+w[j];
此时,第J个物品便有可能加入到一个已经有该物品的最优解中,正好满足完全背包问题的要求.
(完)