lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

USACO是USA Computing Olympiad的简称,它组织了很多面向全球的计算机竞赛活动。

USACO Trainng是一个很适合初学者的题库,我认为它的特色是题目质量高,循序渐进,还配有不错的课文和题目分析。其中关于背包问题的那篇课文 (TEXT Knapsack Problems) 也值得一看。

另外,USACO Contest是USACO常年组织的面向全球的竞赛系列,在此也推荐NOIP选手参加。

我整理了USACO Training中涉及背包问题的题目,应该可以作为不错的习题。其中标加号的是我比较推荐的,标叹号的是我认为对NOIP选手比较有挑战性的。

题目列表

  • Inflate (+) (基本01背包)
  • Stamps (+)(!) (对初学者有一定挑战性)
  • Money
  • Nuggets
  • Subsets
  • Rockers (+) (另一类有趣的“二维”背包问题)
  • Milk4 (!) (很怪的背包问题问法,较难用纯DP求解)

题目简解

以下文字来自我所撰的《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 的不定方程ix+yj=n 必有正整数解,其中n>ij”这一定理得出一个循环的上限。 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 的官 方题解正是这一思路。如果认为以上文字比较难理解可以阅读官方程序或我的程序。

 

 

 

在USACO(美国计算机奥林匹克竞赛)中,有许多题目涉及到背包问题或其变形。例如,以下是一道题目的描述:

Farmer John has N cows (1 ≤ N ≤ 500), each with a weight Wi (1 ≤ Wi ≤ 1000) and a value Vi (1 ≤ Vi ≤ 1000). He wants to bring some of them to the county fair to show off, but his trailer can only hold a total weight of C (1 ≤ C ≤ 10,000). What is the maximum total value of cows that he can bring?

这是一个典型的0-1背包问题,即每个物品(牛)只能选择放入或不放入背包(拖车),而且每个物品只有一个。我们可以用动态规划来求解这个问题。

首先,我们定义一个二维数组dp[i][j]表示前i头牛中,总重量不超过j时,能够获得的最大价值。那么,我们可以得到以下状态转移方程:

  • 如果第i头牛的重量Wi大于j,那么它不能放入背包,所以dp[i][j] = dp[i-1][j]。
  • 如果第i头牛的重量Wi小于等于j,那么它可以选择放入或不放入背包,所以dp[i][j] = max(dp[i-1][j], dp[i-1][j-Wi] + Vi)。

其中,dp[i-1][j]表示不放入第i头牛时的最大价值,dp[i-1][j-Wi] + Vi表示放入第i头牛时的最大价值。

最后,我们需要初始化dp[0][j] = 0表示没有任何牛时的最大价值,以及dp[i][0] = 0表示没有任何容量时的最大价值。那么,我们可以从dp[1][1]开始逐行逐列地填充dp数组,直到得到dp[N][C]为止。这就是我们要求的答案。

import java.io.*;
import java.util.*;

public class Knapsack {
    //定义一个方法,用于求解背包问题
    public static int solveKnapsack(int N, int C, int[] W, int[] V) {
        //定义动态规划数组
        int[][] dp = new int[N+1][C+1];

        //初始化边界条件
        for (int i = 0; i <= N; i++) {
            dp[i][0] = 0;
        }
        for (int j = 0; j <= C; j++) {
            dp[0][j] = 0;
        }

        //填充动态规划数组
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= C; j++) {
                if (W[i] > j) { //第i头牛不能放入背包
                    dp[i][j] = dp[i-1][j];
                } else { //第i头牛可以选择放入或不放入背包
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-W[i]] + V[i]);
                }
            }
        }

        //返回结果
        return dp[N][C];
    }

    public static void main(String[] args) throws IOException {
        //读取输入
        BufferedReader br = new BufferedReader(new FileReader("knapsack.in"));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken()); //牛的数量
        int C = Integer.parseInt(st.nextToken()); //拖车的容量
        int[] W = new int[N+1]; //牛的重量数组
        int[] V = new int[N+1]; //牛的价值数组
        for (int i = 1; i <= N; i++) {
            st = new StringTokenizer(br.readLine());
            W[i] = Integer.parseInt(st.nextToken());
            V[i] = Integer.parseInt(st.nextToken());
        }
        br.close();

        //调用方法,求解背包问题
        int ans = solveKnapsack(N, C, W, V);

        //输出结果
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("knapsack.out")));
        pw.println(ans);
        pw.close();
    }
}

好的,我可以给你一个具体的测试用例。假设输入文件knapsack.in的内容如下:

4 10
5 12
4 3
7 10
2 3

这表示有4头牛,拖车的容量是10,每头牛的重量和价值分别是5和12,4和3,7和10,2和3。那么,输出文件knapsack.out的内容应该是:

15

这表示最大的总价值是15,可以通过选择第一头和第四头牛来达到。你可以用这个测试用例来检验你的代码是否正确。

参考资料:


posted on 2023-06-30 20:58  白露~  阅读(38)  评论(0编辑  收藏  举报