01背包优化

        在上一篇文章中分享了用动态规划来解决01背包问题,使用了二维dp数组,而实际上可以使用一个一维数组,也成为滚动数组来解决01背包问题。从01背包的递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 物品i的体积] + 物品i的价值)可以发现dp[i][j]仅依赖于上一行的某两个数据得出,第i行的数据依赖于i - 1行,因此可以使用滚动数组。
        给出01背包使用滚动数组优化后的代码。

import java.util.*;

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算01背包问题的结果
     * @param V int整型 背包的体积
     * @param n int整型 物品的个数
     * @param vw int整型二维数组 第一维度为n,第二维度为2的二维数组,vw[i][0],vw[i][1]分别描述i+1个物品的vi,wi
     * @return int整型
     */
    public int knapsack (int V, int n, int[][] vw) {
        // write code here
        int[] dp = new int[V + 1];
        for(int i = 0;i < n;i++){
            for(int j = V;j >= 0;j--){
                if(j >= vw[i][0]){
                    dp[j] = Math.max(dp[j], dp[j - vw[i][0]] + vw[i][1]);
                }
            }
        }
        return dp[V];
    }
}

        值得注意的是,使用一维数组优化后,遍历物品价值时,需要倒序遍历,而不是正序遍历,目的是为了避免添加重复的物品。设背包容量为5,物品0的大小为2,价值为10;物品1的大小为3,价值为15;物品2的大小为4,价值为20,使用二维数组vw保存物品的大小和价值。vw[0][0] = 2,vw[0][1] = 10;vw[1][0] = 3,vw[1][1] = 15;vw[2][0] = 4,vw[2][1] = 20。dp[0]背包容量为0,仅有物品0,无法放入物品,dp[0]等于0,dp[1]背包容量为1,仅有物品0,无法放入物品,dp[1]等于0,dp[2]背包容量为2,仅有物品0,可以放入物品0,价值为10,dp[2] = dp[0] + 物品0的价值10,dp[0]等于0,所以价值为10,dp[3],背包容量为3,仅有物品0,可以放入物品0,价值为10,剩余容量1无法放入物品0,dp[3] = dp[1] + 物品0的价值10,dp[1]等于0,所以价值为10,dp[4]背包容量为4,则dp[4] = dp[2] + 物品0的价值,dp[2]等于10,物品0的价值为10,得到dp[4] = 20,添加了两次物品0,但是01背包问题要求仅能添加一次物品,不符合题意,倒序遍历即可解决重复添加物品的问题。
        接着分享两道和01背包相关的Leetcode题。

分割等和子集

给你一个 只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int i = 0;i < nums.length;i++){
            sum += nums[i];
        }
        if(sum % 2 != 0) return false;
        int v = sum / 2;
        int[] dp = new int[v + 1];
        for(int i = 0;i < nums.length;i++){
            for(int j = v;j >= 0;j--){
                if(j >= nums[i]){
                    dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
                }
            }
        }
        return dp[v] == sum / 2;
    }
}

        将一个数组分割成两个相同的子集,先判断数组和是否等于奇数,如果等于奇数,显然无法分成两个相等的子集,如果可以分成两个相等的子集,以上述示例为例,1 + 5 + 11 + 5等于22,可能可以分成子集和为11的两个数组,该问题就可以转化为背包容量为11,数组中元素相当于物品,物品的价值和体积均为数组元素的值,如果可以装满容量为11的背包,那么另一个子集元素和一定是11,因此最后只需要判断是否数组元素能够装满容量为11的背包。

最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回 0。

示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for(int i = 0; i < stones.length;i++){
            sum += stones[i];
        }
        int target = sum / 2;
        int[] dp = new int[target + 1];
        for(int i = 0;i < stones.length;i++){
            for(int j = target;j >= 0;j--){
                if(j >= stones[i]) dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
}

        可以换一种思路,可以将石头数组分为两堆相等或者近似相等的两堆,就是分为相等或近似相等的两个子集,两个相等或者近似相等的两个子集的差是最小的,这一题和上一题非常类似,这一题分不成两个相等的子集也可以,仅要求近似相等,即尽量装满,可能没有装满,最后可以得到一堆石头为dp[target],另一堆则是sum - dp[target],两堆石头的差就是sum - 2 * dp[target]。上述示例中,2 + 7 + 4 + 1 + 8 + 1 = 23,23整除2为11,背包容量为11,stones数组为物品,stones数组中的值既是体积也是价值,stones数组显然可以装满容量为11的背包,则sum - 2 * dp[target] = 23 - 2 * 11 = 1。

参考:带你学透01背包问题(滚动数组篇) | 从此对背包问题不再迷茫!

  • 37
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值