0-1背包问题
且说上一周的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了!
小Ho现在手上有M张奖券,而奖品区有N件奖品,分别标号为1到N,其中第i件奖品需要need(i)张奖券进行兑换,同时也只能兑换一次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。
解题思路:枚举2^N种可能的选取方案,先计算他们需要的奖券之和sum,在sum不超过M的情况下,计算他们的喜好值之和value,并统计一个最优的方案,也就是value的最大值。时间复杂度O(2^N),暴力方法不可取。考虑动态规划。
首先,我们要想办法把我们现在遇到的问题给抽象化。以best(i, j)表示已经决定了前i件物品是否选取,当前已经选取的物品的所需奖券数总和不超过j时,能够获取的最高的喜好值的和。对于任意i>1, j,我们都可以知道best(i, j)=max{best(i-1, j-need(i)) + value(i), best(i - 1, j)}。
检验一下这个问题的定义方法是否拥有动态规划所需要的两种性质:重复子问题和无后效性。
首先看重复子问题——这是动态规划之所以比搜索高效的原因,如果最后四件奖品分别为所需奖券为1,喜好值为1、所需奖券为2,喜好值为2、所需奖券为3,喜好值为3、所需奖券为4,喜好值为4的四个奖品,那么无论是选择1、4还是2、3,都会要求解best(N-4, M-5)这样一个子问题,而这个子问题只需要求解一次就能够进行计算,所以重复子问题这一性质是满足的。
其次再看无后效性……同样的,如果分别有所需奖券为1,喜好值为1、所需奖券为2,喜好值为2、所需奖券为3,喜好值为3、所需奖券为4,喜好值为4的四个奖品,那么无论是选取第1个和第4个,还是选取第2个和第3个,他们的所需奖券数都为5,喜好值之和都为5。所以我只需要知道best(4, 5)=5就够了,它为什么等于5对我而言没有区别,不会对之后的决策产生影响。这就是无后效性,所以想来也是满足的。
那么接下来要考虑的是如何使用best(i, j)=max{best(i-1, j-need(i)) + value(i), best(i - 1, j)}来求解每一个best(i, j)了。我们定义一个问题A依赖于另一个问题B当且仅当求解A的过程中需要事先知道B的值,那么我们很容易的发现best(i, j)是依赖于best(i-1, j-need(i))和best(i-1, j)两个问题的,也就是说这两个问题要先于best(i, j)进行求解。所以我们只要按照i从小到大的顺序,以这样的方式进行计算,就可以了。
时间复杂度已经优化到O(NM)。接下来优化空间复杂度。
按照上面的思路,需要开一个N * M大小的二维数组best,来记录求解出的best值。但是我们在计算i的时候只需要i-1的数据,所以空间可以优化到2*M。进一步,只需要开一个M大小的一维数组就可以了。如果我按照j从M到1的顺序,也就是跟之前相反的顺序来进行计算的话。另外根据我们的状态转移方程,可以显然得出如果状态(iA, jA)依赖于状态(iB, jB),那么肯定有iA = iB+1, jA>=jB。所以不难得出一个结论:我在计算best(i, j)的时候,因为best(i, j+1..M)这些状态已经被计算过了,所以意味着best(i - 1, k),k=j..M这些值都没有用了——所有依赖于他们的值都已经计算完了。于是它们原有的存储空间都可以用来存储别的东西,所以我不仿直接就将best(i, j)的值存在best(i-1, j)原有的位置上。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
while(cin.hasNext()) {
int N = cin.nextInt(); // N个奖品
int M = cin.nextInt(); // M张劵
int[] need = new int[N+1];
int[] value = new int[N+1];
for(int i=1; i<=N; i++) {
need[i] = cin.nextInt();
value[i] = cin.nextInt();
}
int[] f = new int[M+1];
for(int i=1; i<=N; i++) {
for(int j=M; j>=0; j--) {
if(j >= need[i]) {
f[j] = Math.max(f[j], f[j-need[i]] + value[i]);
}
}
}
System.out.println(f[M]);
}
}
}
完全背包问题
且说之前的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了!
等等,这段故事为何似曾相识?这就要从平行宇宙理论说起了………总而言之,在另一个宇宙中,小Ho面临的问题发生了细微的变化!
小Ho现在手上有M张奖券,而奖品区有N种奖品,分别标号为1到N,其中第i种奖品需要need(i)张奖券进行兑换,并且可以兑换无数次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。
思路:按照01背包的想法,我可以使用best(i, j)表示已经决定了前i件物品每件物品选择多少件,当前已经选取的物品的所需奖券数总和不超过j时,能够获取的最高的喜好值的和,那么最终的答案便是best(N, M)。对于一个问题best(i, j),考虑最后一步——即第i件物品选择多少件,不妨就假设选择k件吧,那么k的取值范围肯定是在0~(j / need(i))这个范围内。这个时候我们可以知道best(i - 1, j - need(i) * k) + value(i) * k将会是一种可能的方案。best(i, j) = max(best(i - 1, j - need(i) * k) + value(i) * k),0 <= k <= j / need(i)。我们可以进一步将子结构最优化。将子问题细化到选择第i件物品的每一件,比如我们在选择第i件物品的k件的时候,是可以利用第i件物品的k-1件的信息的。于是,best(i, j) = max(best(i-1, j), best(i, j - need(i)) + value(i))。第一种情况是一件第i件都不选,第二种情况是在i件已经选了k-1的基础上再选一件i。
和0-1背包问题类似,为了优化空间复杂度,我们将j按照从0到M的顺序遍历即可。可以发现,代码与0-1背包问题的唯一区别只有一行。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
while(cin.hasNext()) {
int N = cin.nextInt();
int M = cin.nextInt();
int[] need = new int[N+1];
int[] value = new int[N+1];
for(int i=1; i<=N; i++) {
need[i] = cin.nextInt();
value[i] = cin.nextInt();
}
int[] f = new int[M+1];
for(int i=1; i<=N; i++) {
for(int j=0; j<=M; j++) {
if(need[i] <= j) {
f[j] = Math.max(f[j], f[j-need[i]]+value[i]);
}
}
}
System.out.println(f[M]);
}
}
}