学习笔记 | 算法与数据结构 | 动态规划(背包问题)

经典动态规划:0-1背包问题

【labuladong】0-1背包问题详解
何为背包问题?
形如: 给你一个容量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为wt[i],价值为 val[i],问你用这个背包装物品,最多能装的价值是多少?

第一步 | 关键:【状态】、【选择】

【状态】:【背包剩余容量】、【可选择的物品】
【选择】:当前物品【装入】或者【不装入】背包

动态规划的编程框架(套路):

# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1 的所有取值:
	for 状态2 in 状态2 的所有取值:
		for ...
			dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...)

第二步 | 明确dp数组的定义

dp[i][w] 的含义:
对于 前 i 个物品,当背包容量为 w 时,可以装的最大价值是 dp[i][w]
由此含义=> 确定base case:

  1. 根据 dp[0][0] 的含义:
    对于前 0 个物品,当背包容量为 0 时,可以装的最大价值是 dp[0][0]
    对于前 0 个物品,当背包容量为 0 时,可以装的最大价值显然为 0
    => dp[0][0] = 0

  2. 根据 dp[0][wt] (wt = 1,2,3…)的含义:
    对于前 0 个物品,当背包容量为 任意 wt 时,可以装的最大价值是 dp[0][wt]
    对于前 0 个物品,当背包容量为 任意 wt 时,可以装的最大价值显然为 0
    => dp[0][wt] = 0 (wt = 1,2,3…)

  3. 根据 dp[i][0] ( i = 1,2,3,…) 的含义:
    对于前 0 个物品,当背包容量为 任意 i 时,可以装的最大价值是 dp[i][0]
    对于前 0 个物品,当背包容量为 任意 i 时,可以装的最大价值显然为 0
    => dp[i][0] = 0 ( i = 1,2,3,…)

综上base case: dp[0][…] = dp[…][0] = 0

第三步 | 根据【选择】写出【状态转移方程(逻辑和代码)】

3.1. 状态转移逻辑

int[][] dp[N+1][W+1]
dp[0][...] = 0
dp[...][0] = 0

for i in [1...N]:
	for w in [1...W]:
		dp[i][w] = max(
			把物品 i [装进]背包	# 在 w 的约束下,把物品 i 装进背包, 【最大价值】是多少?
			把物品 i [不装进]背包 # 在 w 的约束下,把物品 i 不装进背包, 【最大价值】是多少?
		)
return dp[N][W]

注意【索引偏移】问题:
因为会考虑第0个物品,0的背包剩余容量,所以dp数组行和列各多1个;
但实际上, val[0] 和 wt[0] 都是表示第1个物品的价值和重量;
即,在遍历dp数组中的 dp[i][…] 表示第 i 个物品,其价值为 val[i-1], 重量为 wt[ i -1]

3.2. 逻辑转为代码的关键:考虑以下两个问题,接着根据dp数组定义推导出代码

  • w 的约束下,把物品 i 装进背包, 【最大价值】是多少?
  • w 的约束下,把物品 i 不装进背包, 【最大价值】是多少?

即有

  • w 的约束下,把物品 i 装进背包, 【最大价值】是多少?
    => dp[i][w] = dp[i][w - wt[i]] + val[i]
  • w 的约束下,把物品 i 不装进背包, 【最大价值】是多少?
    => dp[i][w] = dp[i-1][w]


dp[i][w] = max(dp[i][w - wt[i]] + val[i] , dp[i-1][w])

3.3. 状态转移代码
在这里插入图片描述

背包问题变体:子集背包问题

本节内容:

  1. 体会 0-1背包思想 如何运用到 具体的题目上;
  2. 学习如何将二维动态规划 压缩为 一维动态规划。

力扣 第 416 题「 分割等和子集」

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

对于这个问题,我们可以先对集合求和,得出 sum,把问题转化为背包问题:

给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

就是背包问题的模型,甚至比我们之前的经典背包问题还要简单一些,下面就直接转换成背包问题,开始套前文讲过的背包问题框架即可。

第一步:明确状态和选择
【状态】:【背包剩余容量】、【可选择的物品】
【选择】:当前物品【装入】或者【不装入】背包

第二步:明确dp数组意义、初始化
dp[i][j] 的含义:
对于前 i 个物品,能否凑出 j 的总重量。若 dp[i][j] = true ,说明能;为false则不能

base case:
dp[0][0] = true
对于前0 个物品,可以凑出0 的总重量

dp[0][j] = false (j = 1,…,sum/2)
对于前0 个物品,没法凑出大于 0 的总重量

dp[i][0] = true (i = 1,…,N)
对于任意个物品,可以凑出 0 的总重量

=> 综上,basecase为 dp[0][j] = false (j = 1,…,sum/2) 和 dp[i][0] = true (i = 0,…,N)

第三步:明确状态转移方程,写出代码

// 关键代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        ...
		// 将问题转化为背包问题
        for(int i = 0; i<n;i++){
            sum = sum + nums[i];
        }
        if(sum%2 != 0){
            return false;
        }
        half = sum / 2;
        vector<vector<bool>> dp(n+1,vector<bool>(half+1));
        // base case:
        for(int i = 0;i<=n;i++){
            dp[i][0] = true;
        }
        for(int j = 1; j<= half;j++){
            dp[0][j] = false;
        }
        // 状态转移逻辑
        for(int i = 1; i <= n; i++){
            for(int j = 1; j< half+1;j++){
                if(nums[i-1] > j){
                    dp[i][j] = dp[i-1][j];
                }else{
                    dp[i][j] = (dp[i-1][j] || dp[i-1][j-nums[i-1]]);
                }
                
            }
        }
        return dp[n][half];
    }
};

第四步 | 考虑【空间优化】

一般空间优化(降维打击)都是用在二维dp上(一维再压就没有了)。那么什么时候能够对二位动态dp的空间进行优化呢?——
观察其状态转移方程,若dp[i][j] 依赖的都是其相邻状态,则可以使用空间压缩的技巧进行空间优化。

完全背包问题

力扣 第 518 题「 零钱兑换2 」

完全背包问题和上述的两种背包问题(经典、子集背包)最大的不同在于:每个物品数量无限

c++的代码执行错误,java没问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值