[动态规划] 0-1背包问题和完全背包问题

背包问题

背包问题(Knapsack problem)是一种组合优化的 NP 完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中,背包的空间有限,但我们需要最大化背包内所装物品的价值。背包问题通常出现在资源分配中,决策者必须分别从一组不可分割的项目或任务中进行选择,而这些项目又有时间或预算的限制。https://zh.m.wikipedia.org/zh/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98

参考:
Y总《背包九讲》
代码随想录
书:《图解算法》Aditya Bhargava

  1. 0-1 背包
  2. 完全背包问题

1. 0-1 背包

N N N 件物品和一个容量为 V V V 的背包。第 i i i 件物品的体积是 C i C_i Ci,价值是 W i W_i Wi
求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

暴力法
列举每种情况,时间复杂为 O ( 2 N ) O(2^N) O(2N)

动态规划1
dp[i][j] 表示 [0, i] 物品任意取放入到容量为 j 的背包里的最大价值。

dp 数组初始化:dp[i][0] 均为 0dp[0][0~物品 0 的体积-1] 均为 0
任取 [ 0 , i ] 物品 , 体积为 j 时的最大价值 = m a x { 任取物品 [ 0 , i − 1 ] , 且体积为 j 最大价值 任取物品 [ 0 , i − 1 ] , 体积为 ( j − 物品 i 的体积 ) 时的最大价值 + 物品 i 的价值 任取[0,i]物品,体积为 j 时的最大价值= max \left\{ \begin{aligned} & 任取物品[0,i-1],且体积为 j 最大价值 \\ & 任取物品[0,i-1],体积为 (j - 物品 i 的体积)时的最大价值 + 物品 i 的价值 \end{aligned} \right. 任取[0,i]物品,体积为j时的最大价值=max{任取物品[0,i1],且体积为j最大价值任取物品[0,i1],体积为(j物品i的体积)时的最大价值+物品i的价值
转换为状态转移公式:dp[i][j] = max(dp[i-1][j], dp[i-1][ j-w[i] ] + w[i])

dp 数组最后一个元素即所求的最大价值。

时间复杂为 O ( N V ) O(NV) O(NV)

0-1背包问题模板

模板一:

int 01Package(int[] capacities, int[] worth, int max_capacity) {
        // n 表示物品数量
        int n = capacities.length;
        int[] dp = new int[n][max_capacity + 1];
        // java 默认初始化全部元素为 0,所以只需要初始化第 0 行即可,不需要关心第 0 列
        for (int j = 1; j <= max_capacity; j++) {
            if (j >= capacities[0]) {
                dp[0][j] = worth[0];
            }
        }

        // 从第 1 行和第 1 列开始遍历更新数组
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= max_capacity; j++) {
                // 状态转移方程
	            dp[i][j] = dp[i-1][j];
	            // 防止越界
	            if (j >= capacities[i]) {
	                dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-capacities[i]] + worth[i]);    
	            }
            }   
        }
        // 最大价值
        return dp[n-1][max_capacity];
}

0-1背包问题模板2【一维滚动数组】:

dp[j] 表示容量为 j 的背包所背的最大价值。
dp 数组初始化为 0
转移方程为:dp[j] = max(dp[j], dp[j-capacities[i]] + worth[i])

  • 因为二维数组的情况 dp 数组中元素的改变只跟正上方和左上方的数据有关,上下两行数据不相关,所以遍历容量的时候可以正序也可以倒序遍历。而只使用一维数组数组时,若容量正序遍历的时候会造成一个商品选择多次的情况,破坏了上一次选择的状态,倒序的避免了这一情况,每次更新都使用上一层的状态。
  • 二维数组先遍历物品和先遍历容量都是可以的,而一维数组先倒序遍历容量的话,结果只能得到满足容量的单个物品价值。
int[] dp = new int[max_capacity + 1];

// 初始化
for (int i = 0; i <= max_capacity; i++) {
	dp[i] = 0;
}

//01背包
for (int i = 0; i < n; i++) {
	// 当前dp[i]要使用上一层左侧的dp值,正序覆盖了上一层左侧的dp值,可能造成多次选取物品的情况,倒叙则避免了这一情况
    for (int j = max_capacity; j >= capacities[i]; j--) {
        dp[j] = Math.max(dp[j], dp[j-capacities[i]] + worth[i]);
    }
}

LeetCode 416. 分割等和子集

https://leetcode.cn/problems/partition-equal-subset-sum/submissions/
可以把问题抽象为:给定一个数组和一个容量为 sum / 2 的背包,求是否有一种组合能让背包装满。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int x : nums) {
            sum += x;
        }

        if (sum % 2 == 1) {
            return false;
        }
        
        int target = sum / 2;
        
        int[] dp = new int[target + 1];

        for (int num : nums) {
            for (int j = target; j >= num; j--) {
                // 状态转移方程
                dp[j] = Math.max(dp[j], dp[j-num] + num);
            }   
        }
        
        return dp[target] == target;
    }
}

LeetCode 494. 目标和

https://leetcode.cn/problems/target-sum/
由于 nums 数组中的元素全为自然数,原问题可转换为:
找到 nums 一个正子集和一个负子集,使得总和等于 target,即

sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
2 * sum(P) = target + sum(nums)

最终转换为求 0-1 背包问题
给定一个数组和一个容量为 (target + sum(nums)) / 2 的背包,求有多少种组合能让背包装满。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 以下三种情况表示无解
        if (sum < target || (sum + target) % 2 == 1 || target + sum < 0) {
            return 0;
        }
        
        int max_capacity = (sum + target) / 2;
        int dp[] = new int[max_capacity + 1];
		
		// 初始化 dp[0] = 1
        dp[0] = 1;

        for (int num : nums) {
            for (int j = max_capacity; j >= num; j--) {
                dp[j] += dp[j - num];
            }
        }

        return dp[max_capacity];
    }
}

递归回溯法,时间复杂度太高了 O ( 2 n ) O(2^n) O(2n)

class Solution {
    private int dfs(int[] nums, int start, int S) {
        if (start == nums.length) {
            return S == 0 ? 1 : 0;
        }
        return dfs(nums, start + 1, S + nums[start])
                + dfs(nums, start + 1, S - nums[start]);
    }

    public int findTargetSumWays(int[] nums, int S) {
        return dfs(nums, 0, S);
    }
}

2. 完全背包问题

N N N 种物品和一个容量为 V V V 的背包。第 i i i 件物品的体积是 C i C_i Ci,价值是 W i W_i Wi,每种物品可以重复选择,
求解将物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

完全背包问题模板

在 0-1 背包问题一维数组解法的基础上,将价值数组正序遍历,表示可以重复选取物品,即可得到完全背包问题的解法。

dp 数组初始化为 0
转移方程:dp[j] = max(dp[j], dp[j-capacities[i]] + worth[i])

int[] dp = new int[max_capacity + 1];

// 初始化
for (int i = 0; i <= max_capacity; i++) {
	dp[i] = 0;
}
    
//完全背包
for (int i = 0; i < n; i++) {
	// 当前dp[i]要使用上一层左侧的dp值,在上一节的基础上正序遍历,多次选取物品
    for (int j = capacities[i]; j <= max_capacity; j++) {
        dp[j] = Math.max(dp[j], dp[j-capacities[i]] + worth[i]);
    }
}

LeetCode 518. 零钱兑换 II

https://leetcode.cn/problems/coin-change-2/
此题与 LeetCode 494. 目标和 类似,求能装满背包的共有多少种装法。

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;

        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                dp[j] += dp[j - coin];
            }
        }

        return dp[amount];
    }
}

LeetCode 322. 零钱兑换

https://leetcode.cn/problems/coin-change/
dp[j]: 表示凑足总额为 j 所需钱币的最少个数为 dp[j]
初始化 dp[0] = 0 表示凑足总额为 0 所需钱币的个数为 0
初始化 dp[1~amount] = MAX_VALUE 表示没有凑足总额为 1~amount 的钱币搭配。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        dp[0] = 0;

        int MAX_INT = Integer.MAX_VALUE;
        for (int i = 1; i <= amount; i++) {
            dp[i] = MAX_INT;
        }

        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                if (dp[j-coin] != MAX_INT) {
                    dp[j] = Math.min(dp[j], dp[j-coin] + 1);
                }
            }
        }

        return dp[amount] == MAX_INT ? -1 : dp[amount];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇咔咔负负得正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值