动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(一)

简介: 动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(一)

背包三讲


复现代码随想录DP专题


代码随想录 (programmercarl.com)

动态规划五部曲


确定dp数组以及下标的含义

确定递推公式

dp数组如何初始化

确定遍历顺序

打印数组(与自己的推导比较,看哪里错了)


01背包

有N件物品和⼀个最多能被重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装⼊背包里物品价值总和最大?


1. 例题(二维数组解法)

题目

背包最大重量为4


重量

价值

物品0

1

15

物品1

3

20

物品2

4

30

问:背包能背的物品最大价值是多少?


思路

五部曲


数组含义


使用二维数组 dp[i][j],含义:从下标为 [ 0 ∼ i ] 的物品中任意取,放入容量为 j jj 的背包,价值总和最大为多少

递推公式由两个方向推出


一、不放物品 i的最大价值

背包容量为 j,但不放物品 i  时的最大价值,即 dp[i-1][j]

二、放物品 i的最大价值

先找到 dp[i-1][j-weight[i]],含义:i − 1 保证了不放物品 i,背包容量为 j − w e i g h t [ i ]其实就是为了给后续放物品 i  一个预留量,保证放了物品 i ii 后背包不会溢出,所以最大价值为 dp[i-1][j-weight[i]]+value[i]

递推公式:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] )


数组初始化


背包容量 j 为零时,显然价值为零

只选物品0,即第一行,显然只要物品0重量小于等于背包重量 j,价值就为物品0的价值,否则为零

确定遍历顺序


先遍历背包还是物品都可以

dp[i][j] 所需的数据在其左上方

测试用例



代码展示

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
void test_2_wei_bag_problem1()
{
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;
    vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1));
    for (int i = 1; i < bagWeight + 1; i++)
    {
        if (weight[0] <= i)
        {
            dp[0][i] = value[0];
        }
    }
    for (int j = 1; j < bagWeight + 1; j++)
    {
        for (int i = 1; i < weight.size(); i++)
        {
            if (j < weight[i])
            {
                dp[i][j] = dp[i - 1][j];
            }
            else
            {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
            }
        }
    }
    cout << dp[weight.size() - 1][bagWeight];
}
int main()
{
    test_2_wei_bag_problem1();
    return 0;
}


2. 例题(滚动数组解法)

还是之前的例子,可以用滚动数组将数组降为一维

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] )

由递推公式得: dp[i][j] 只和它的上一行有关,且有关元素的行数为 i − 1,列数 ≤ j 。那么我们就可以将数组压缩成一维

dp[i1][jweight[i]]

dp[i1][j]


欲求元素

看这张表,如果数组是一维的情况,那么在算出欲求元素后,其实是将欲求元素覆盖掉 dp[i-1][j],并且我们的遍历顺序也要改变。在二维情况下,我们是按从左到右的顺序求的,但在一维中,必须从右向左!因为在求欲求元素时,我们要保证 dp[i-1][j-weight[i]] 未被覆盖!同时,必须按照先遍历物品,再嵌套遍历背包的顺序。


01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次


举例:


假设物品0重量 w e i g h t [ 0 ] = 1 ,价值 v a l u e [ 0 ] = 15


如果是正序遍历


d p [ 1 ] = d p [ 1 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] = 15


d p [ 2 ] = d p [ 2 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] = 30


在第一行运行后 d p [ 1 ] 的状态已经发生改变,意味着已经放了物品0,而第二行运行后,叠加了改变后的 d p [ 1 ] ,意味着物品0被放了两次。


那么为什么之前在写二维数组的时候是正序的?


因为我们实际求的是上一行的状态,也就是不放当前物品的情况,只不过在滚动数组中,压缩了状态。也即当前元素的公式被运行后,当前元素的状态(在当前行,包括物品0)覆盖了先前状态(在上一行,不含物品0),所以一维中倒序是为了保证物品只被放入一次!

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
void test_1_wei_bag_problem1()
{
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;
    vector<int> dp(bagWeight + 1);
    for (int i = 0; i < weight.size(); i++)
    {
        for (int j = bagWeight; j >= weight[i]; j--)
        {
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight];
}
int main()
{
    test_1_wei_bag_problem1();
    return 0;
}


3. 分割等和子集(LeetCode-416)

题目

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


示例 1:

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


示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。


提示:


1 <= nums.length <= 200

1 <= nums[i] <= 100


思路

完全背包和01背包区别?


完全背包中一个商品可以放 无数次

01背包中一个商品只能放一次


本题如何套?


分割成两个子集且元素和相等,即背包容量为 s u m / 2 sum/2sum/2

物品重量为 n u m s [ i ] nums[i]nums[i],价值也为 n u m s [ i ] nums[i]nums[i]

背包正好装满,就说明找到了 s u m / 2 sum/2sum/2


五部曲


dp[i] 含义


背包容量为 i 时,最大可以凑出的子集和

递推公式


本题中,物品重量为 n u m s [ i ] ,价值也为 n u m s [ i ]

d p [ j ] = m a x ( d p [ j ] , d p [ j − n u m s [ i ] ] + n u m s [ i ] )


数组初始化


都初始化为零

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序,不懂的回看例题

测试用例



代码展示

class Solution
{
public:
    bool canPartition(vector<int> &nums)
    {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2)
        {
            return false;
        }
        int target = sum / 2;
        vector<int> dp(target + 1);
        for (int i = 0; i < nums.size(); i++)
        {
            for (int j = target; j >= nums[i]; j--)
            {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
                if (dp[target] == target)
                {
                    return true;
                }
            }
        }
        return false;
    }
};


对比卡尔哥的解法,用 target+1 做为数组大小,优化了空间。遍历中遇到一个吻合的直接返回 true,优化了时间。


4. 最后一块石头的重量Ⅱ(LeetCode-1049)

题目

有一堆石头,用整数数组 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],这就是最优值。


示例 2:

输入:stones = [31,26,33,21,40]
输出:5


示例 3:

输入:stones = [1,2]
输出:1


提示:


1 <= stones.length <= 30

1 <= stones[i] <= 100


思路

如何套?


目的是求 最小的可能重量,其实也就是凑重量尽可能相等的两堆。举个例子 10,1,2,2,2,2,2 。我们就可以分为重量为 11 1111 和 10 1010 的两堆。


五部曲


dp[j] 含义


重量为 j jj 的背包最大可以装的石头重量和

递推公式


本题中,物品重量为 s t o n e s [ i ] ,价值也为 s t o n e s [ i ]

d p [ j ] = m a x ( d p [ j ] , d p [ j − s t o n e s [ i ] ] + s t o n e s [ i ] )


数组初始化


设默认值零即可

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序

测试用例



代码展示

class Solution
{
public:
    int lastStoneWeightII(vector<int> &stones)
    {
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int target = sum / 2;
        vector<int> dp(target + 1);
        for (int i = 0; i < stones.size(); i++)
        {
            for (int j = target; j >= stones[i]; j--)
            {
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};


开始时没有考虑到问题是最小的可能重量。在 stones = [31,26,33,21,40] 这个样例中:一堆为 31,26,21 重量和为 78 7878,另一堆为 33,40 重量和为 73 7373。按照代码求法,target=75。求的是 dp[75]。为什么不是求 dp[73]?回看数组定义:重量为 j jj 的背包最大可以装的石头重量和。可以看出:其实 dp[75] 与 dp[73] 是相等的


5. 目标和(LeetCode-494)

题目

给你一个整数数组 nums 和一个整数 target 。


向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :


例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。


示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3


示例 2:

输入:nums = [1], target = 1
输出:1


提示:


1 <= nums.length <= 20

0 <= nums[i] <= 1000

0 <= sum(nums[i]) <= 1000

-1000 <= target <= 1000


思路

怎么套?


分成两堆, l e f t − r i g h t = t a r g e t


又因为 l e f t + r i g h t = s u m


所以 l e f t = ( s u m + t a r g e t )/ 2


这和以前遇到的题目 不一样


先前:容量为 i 的背包,最多能装多少?


这次:填满容量为 i 的背包,有多少种方法?


五部曲


数组定义


dp[j] 表示:填满容量为 j 的背包,有 dp[j] 种方法

递推公式


分两种情况,一种是不包含当前物品的方法数量,另一种是包含当前物品的方法数量。我们要求的就是二者 之和。为了方便,直接使用一维数组展示:

d p [ j ] = d p [ j ] + d p [ j − n u m s [ i ] ]

数组初始化


如果是二维数组,也就是要初始化第一行,即只有物品零的情况,可以想见,填满背包容量为零的方法有一种,就是不装东西。但填满不为零的背包的方法却为零,除非物品重量等于背包容量。所以在一维数组中初始化应该是 dp[0]=1 其他为0

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序

测试用例



代码展示

class Solution
{
public:
    int findTargetSumWays(vector<int> &nums, int target)
    {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if ((sum < target) || ((sum + target) % 2) || ((sum + target) < 0))
        {
            return 0;
        }
        int left = (sum + target) / 2;
        vector<int> dp(left + 1);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++)
        {
            for (int j = left; j >= nums[i]; j--)
            {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[left];
    }
};
目录
相关文章
|
索引 Python
Python 教程之 Pandas(10)—— 访问 series 的元素
Python 教程之 Pandas(10)—— 访问 series 的元素
355 0
Python 教程之 Pandas(10)—— 访问 series 的元素
|
SQL 关系型数据库 MySQL
解决MySQL主从慢同步问题的常见的解决方案:
解决MySQL主从慢同步问题的方法有很多,以下是一些常见的解决方案: 1. 检查网络连接:确保主从服务器之间的网络连接稳定,避免网络延迟或丢包导致数据同步缓慢。 2. 优化数据库配置:调整MySQL的配置参数,如增大binlog文件大小、调整innodb_flush_log_at_trx_commit等参数,以提高主从同步性能。 3. 检查IO线程和SQL线程状态:通过SHOW SLAVE STATUS命令检查IO线程和SQL线程的状态,确保它们正常运行并没有出现错误。 4. 检查主从日志位置:确认主从服务器的binlog文件和位置是否正确,避免由于错误的日志位置导致同步延迟。 5.
1853 1
|
7月前
|
移动开发 小程序 安全
微信API社交裂变工具,老带新流量成本归零!
在数字化营销时代,利用微信API构建社交裂变工具,可实现“老带新”的病毒式传播,大幅降低获客成本。本文详解如何通过微信API实现零成本流量增长,解析裂变机制与技术实现。
357 0
|
SQL 监控 数据库
SQL语句性能分析技巧与方法
在数据库管理中,分析SQL语句的性能是优化数据库查询、提升系统响应速度的重要步骤
|
存储 算法 数据可视化
【Python】实现二维装箱Bottom-Left算法及用人工蜂群算法改进
本文介绍了二维装箱问题的Bottom-Left算法,并提供了Python实现,包括主函数、装箱顺序、重叠检测、最终位置计算等,同时指出了算法的缺点并提出了使用人工蜂群算法进行改进的方法,最后提供了完整代码的下载链接。
1117 1
BXA
|
算法 程序员 决策智能
动态规划详解背包问题及实践(附C++代码)
背包问题是一个经典的组合优化问题,它可以被抽象为一个把物品放入背包中的过程,以求最终背包中物品价值的最大化
BXA
1497 0
|
网络协议 网络架构
动态图解 | 9分钟让你明明白白看懂Traceroute(路由追踪)的原理与实现
动态图解 | 9分钟让你明明白白看懂Traceroute(路由追踪)的原理与实现
2593 1
|
消息中间件 Kafka
kafka里的acks是什么
【8月更文挑战第3天】kafka里的acks是什么
886 0
|
算法 机器人 Python
Python实现教程:平面最短路径算法
Python实现教程:平面最短路径算法
505 1
|
算法 Java Go
非启发式算法——旅行商问题(TSP)及其解决算法
非启发式算法——旅行商问题(TSP)及其解决算法
1579 0

热门文章

最新文章