hihoCode----背包问题

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]);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值