C++算法 —— 动态规划(8)01背包问题

news2025/1/12 10:52:50

文章目录

  • 1、动规思路简介
  • 2、模版题:01背包
    • 第一问
    • 第二问
    • 优化
  • 3、分割等和子集
  • 4、目标和
  • 5、最后一块石头的重量Ⅱ


背包问题需要读者先明白动态规划是什么,理解动规的思路,并不能给刚接触动规的人学习。所以最好是看了之前的动规博客,才能看背包问题的所有博客,或者你本人就已经懂得动规了。

1、动规思路简介

动规的思路有五个步骤,且最好画图来理解细节,不要怕麻烦。当你开始画图,仔细阅读题时,学习中的沉浸感就体验到了。

状态表示
状态转移方程
初始化
填表顺序
返回值

动规一般会先创建一个数组,名字为dp,这个数组也叫dp表。通过一些操作,把dp表填满,其中一个值就是答案。dp数组的每一个元素都表明一种状态,我们的第一步就是先确定状态。

状态的确定可能通过题目要求来得知,可能通过经验 + 题目要求来得知,可能在分析过程中,发现的重复子问题来确定状态。还有别的方法来确定状态,但都大同小异,明白了动规,这些思路也会随之产生。状态的确定就是打算让dp[i]表示什么,这是最重要的一步。状态表示通常用某个位置为结尾或者起点来确定。

状态转移方程,就是dp[i]等于什么,状态转移方程就是什么。像斐波那契数列,dp[i] = dp[i - 1] + dp[i - 2]。这是最难的一步。一开始,可能状态表示不正确,但不要紧,大胆制定状态,如果没法推出转移方程,没法得到结果,那这个状态表示就是错误的。所以状态表示和状态转移方程是相辅相成的,可以帮你检查自己的思路。

要确定方程,就从最近的一步来划分问题。

初始化,就是要填表,保证其不越界。像第一段所说,动规就是要填表。比如斐波那契数列,如果要填dp[1],那么我们可能需要dp[0]和dp[-1],这就出现越界了,所以为了防止越界,一开始就固定好前两个值,那么第三个值就是前两个值之和,也不会出现越界。初始化的方式不止这一点,有些问题,假使一个位置是由前面2个位置得到的,我们初始化最一开始两个位置,然后写代码,会发现不够高效,这时候就需要设置一个虚拟节点,一维数组的话就是在数组0位置处左边再填一个位置,整个dp数组的元素个数也+1,让原先的dp[0]变为现在的dp[1],二维数组则是要填一列和一行,设置好这一行一列的所有值,原先数组的第一列第一行就可以通过新填的来初始化,这个初始化方法在下面的题解中慢慢领会。

第二种初始化方法的注意事项就是如何初始化虚拟节点的数值来保证填表的结果是正确的,以及新表和旧表的映射关系的维护,也就是下标的变化。

填表顺序。填当前状态的时候,所需要的状态应当已经计算过了。还是斐波那契数列,填dp[4]的时候,dp[3]和dp[2]应当都已经计算好了,那么dp[4]也就出来了,此时的顺序就是从左到右。还有别的顺序,要依据前面的分析来决定。

返回值,要看题目要求。

背包问题有很多种分类,此篇是关于01背包问题的。背包问题在写完代码都需要再做优化。所以比起之前的动规算法博客,背包问题的几篇博客都在最后加上一步优化。但优化方法只在模版题写,其它不写了。

2、模版题:01背包

DP41 【模板】01背包

在这里插入图片描述
在这里插入图片描述

第一问

先定dp[i]为从前i个物品选,选出最大价值的,这个其实是不行的,如果要选第i个物品,就需要看看背包满没满,但是现在的状态没法表示体积,所以不行。那就用二维数组,dp[i][j]表示从前i个物品中挑选,总体积不超过j,所有选法中,能选出的最大价值。

状态转移方程。根据最后一步来分析。我们可以选择第i个物品,也可以不选。如果不选,就是在0到i- 1中选,这个的结果放在dp[i -1][j]中;如果选择i位置的元素,也就是体积加上vi,那为了不超过背包,就得选择dp[i - 1][j - vi]位置的值加上wi,这个情况还有考虑,可能vi大于j了,所以j - vi需要>= 0。所以dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j - vi] + wi)。

初始化,要加上一行一列,里面的值全都为0。返回值是最大值。

第二问

在第一问基础上来做改动。之前的dp[i][j]要从不超过j改成正好等于j。状态转移方程中,有可能选上一个后,仍然不够j。如果不选第i个物品,那么就是dp[i - 1][j],意思是在前i - 1个物品中选,体积等于j的最大价值,但有可能是达不到j的,我们先定义dp[i][j] = -1来表示没有这种情况,也就是前i个物品中没有能达到体积为j的选法。如果dp[i - 1][j]是-1,那么不选i位置的物品还是体积达不到j,所以不选的话就不用考虑了。选第i个物品的话,就要先看看dp[i - 1][j - vi]存不存在,不等于-1,也就是说前i - 1个位置正好达到了j - vi的体积,那么加上第i个物品就可行了。

初始化。新增行列,第一列整体都是0,第一行其余都是-1。

#include <iostream>
#include <cstring>
using namespace std;

const int N = 1010;

int n, V, v[N], w[N];
int dp[N][N];

int main()
{
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V] << endl;//第一问
    memset(dp, 0, sizeof(dp));
    for(int j = 1; j <= V; j++) dp[0][j] = -1;
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i] && dp[i - 1][j - v[i]] != -1) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}

优化

数组很大,用滚动数组的思路来优化。仔细看一下分析,dp[i][j]由dp[i - 1][j]和dp[i - 1][j - vi]来决定,也就是这一行的一个元素由上一行的两个元素来决定,和其它行都没有关系,那么我们仅需要两行其实就是可以完成dp表的填写,根据第一行填完第二行后,第一行作为原先的第二行,第二行作为新的第二行继续更新数据。这就是滚动数组。但还可以减少空间。只用一行来进行操作。dp[j]由dp[j]和dp[j - vi]来决定,然后更新dp[j],用一行的话填表顺序就不能是从左到右,因为dp[j - vi]会把dp[j - vi]给换掉,所以填表顺序应当是从右到左,才能保证每个值都正确更新。

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

const int N = 1010;

int n, V, v[N], w[N];
int dp[N];

int main()
{
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = V; j >= v[i]; j--)//原本是j >= 1,在下面判断,但其实可以直接放在for括号里,减少循环次数
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[V] << endl;//第一问
    memset(dp, 0, sizeof(dp));
    for(int j = 1; j <= V; j++) dp[j] = -1;
    for(int i = 1; i <= n; i++)
    {
        for(int j = V; j >= v[i]; j--)
        {
            if(dp[j - v[i]] != -1)
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
    return 0;
}

3、分割等和子集

416. 分割等和子集

在这里插入图片描述

可以转化成挑选一个数来达到是整体数组和的一半这个条件。并且,其实就是从前i个数中选,所有的选法中,能否凑齐j这个数,类型为bool,j就是sum / 2。

状态转移方程。如果不选i,那就看dp[i - 1][j];如果选i,那就是拿前面值加上i位置的值,假设前面的值是numi,那么这个位置应当在j - numi位置,但是j - numi必须存在才行。

初始化新增一行一列,第一列都是true,第一行其余位置都是false。

返回值是dp[n][sum / 2]。

    bool canPartition(vector<int>& nums) {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false;
        int aim = sum / 2;
        vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));
        for(int i = 0; i <= n; i++) dp[i][0] = true;
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= aim; j++)
            {
                dp[i][j] = dp[i - 1][j];
                if(j >= nums[i - 1])
                    dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
        return dp[n][aim];
    }

但是这种做法实在不妥,按照滚动数组优化一下。

    bool canPartition(vector<int>& nums) {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false;
        int aim = sum / 2;
        vector<bool> dp(aim + 1);
        dp[0] = true;
        for(int i = 1; i <= n; i++)
        {
            for(int j = aim; j >= nums[i - 1]; j--)
            {
                dp[j] = dp[j] || dp[j - nums[i - 1]];
            }
        }
        return dp[aim];
    }

4、目标和

494. 目标和

在这里插入图片描述
根据题目,这些数字会被分为正数和负数,假设正数是a,负数绝对值是b,也就是说a + b = sum,a - b = target,所以a = (t + s) / 2,此时b就不见了。那么只用看a,也就是说选一些数,和正好是a,有多少种选法,就是答案。dp[i][j]就表示从前i个数中选,总和正好等于i,一共有多少种选法。

状态转移方程。如果选i,那么就看dp[i - 1][j nums[i]],因为是方法个数,所以不需要+nums[i];不选择i的话,就看dp[i - 1][j]。

初始化时,新增行列都是0,除了dp[0][0]是1。

返回值是最后一个值。优化部分直接写到代码上。

    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(auto x: nums) sum += x;
        int aim = (sum + target) / 2;
        if(aim < 0 || (sum + target) % 2) return 0;
        int n = nums.size();
        vector<int> dp(aim + 1);
        dp[0] = 1;
        for(int i = 1; i <= n; i++)
        {
            for(int j = aim; j >= nums[i - 1]; j--)
            {
                dp[j] += dp[j - nums[i - 1]];
            }
        }
        return dp[aim];
    }

5、最后一块石头的重量Ⅱ

1049. 最后一块石头的重量 II

在这里插入图片描述

分析题目。每次拿到两个数字,按照题目去操作,其实就相当于一个正数和一个负数相加,得到一个值,是0就都没了,那么元素多了的话,就和上一题相似,一堆正数和一堆负数之间的运算。假设正数是a,负数的绝对值和是b,那么a + b = sum,求最小的a - b的值。从sum角度看,一个数字可以分成两个数字相加,如果这两个数字越接近sum的一半,那么就差值就越小。所以最终这个问题就转换成在数组中选择一些数,让这些数的和尽可能地接近sum / 2。

让dp[i][j]表示从前i个数中选,总和不超过j,此时的最大和。这个j就是sum / 2。如果选i,那就是dp[i - 1][j - nums[i]] + nums[i],如果不选i,那就是dp[i - 1][j],然后选出最大值。

初始化,新增行列都是0就可。

返回值返回sum - 2* dp[n][sum / 2]。优化直接写出来。

    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(auto x : stones) sum += x;
        int n = stones.size(), m = sum / 2;
        vector<int> dp(m + 1);
        for(int i = 1; i <= n; i++)
        {
            for(int j = m; j >= stones[i - 1]; j--)
            {
                dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
            }
        }
        return sum - 2 * dp[m];
    }

结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1058346.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】排序(2)—冒泡排序 快速排序

目录 一. 冒泡排序 基本思想 代码实现 时间和空间复杂度 稳定性 二. 快速排序 基本思想 代码实现 hoare法 挖坑法 前后指针法 时间和空间复杂度 稳定性 一. 冒泡排序 基本思想 冒泡排序是一种交换排序。两两比较数组元素&#xff0c;如果是逆序(即排列顺序与排序后…

递归与分治算法(1)--经典递归、分治问题

目录 一、递归问题 1、斐波那契数列 2、汉诺塔问题 3、全排列问题 4、整数划分问题 二、递归式求解 1、代入法 2、递归树法 3、主定理法 三、 分治问题 1、二分搜索 2、大整数乘法 一、递归问题 1、斐波那契数列 斐波那契数列不用过多介绍&#xff0c;斐波那契提出…

行高的继承和消除内外边距

行高的继承性 <style>div {font: 12px/1.5 Microsoft yahei;} ​p {font-size: 14px;}</style> <body><div><p>苏丹红事件</p></div> <body> 12px这里没有行高没有写单位&#xff0c;子类继承父类的1.5倍&#xff0c;就是14*…

AndroidStudio精品插件集

官网 项目地址&#xff1a;Github博客地址&#xff1a;Studio 精品插件推荐 使用需知 所有插件在 Android Studio 2022.3.1.18&#xff08;长颈鹿&#xff09;上测试均没有问题&#xff0c;推荐使用此版本Android Studio 2022.3.1.18&#xff08;长颈鹿&#xff09;正式版下…

Django之十二:模板的继承+用户列表

模板的继承 新建layout.html&#xff1a; {% load static %} <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"{% static plugins…

经典网络架构-ResNet

# 引言 深度残差网络的提出是深度学习领域的里程碑事件&#xff0c;它使得网络可以做大做深&#xff0c;现在主流的网络中都有残差结构 # 问题 - ##深度网络的退化 深度网络有一个普遍的问题&#xff1a;随着网络层数的增加&#xff0c;准确率是先增后减的&#xff0c;准确率增…

十四天学会C++之第二天(函数和库)

1. 函数的定义和调用 在C中&#xff0c;函数是组织和结构化代码的关键工具之一。它们允许您将一段代码封装成一个可重复使用的模块&#xff0c;这有助于提高代码的可读性和维护性。 为什么使用函数&#xff1f; 函数在编程中的作用不可小觑。它们有以下几个重要用途&#xf…

【数据结构---排序】很详细的哦

本篇文章介绍数据结构中的几种排序哦~ 文章目录 前言一、排序是什么&#xff1f;二、排序的分类 1.直接插入排序2.希尔排序3.选择排序4.冒泡排序5.快速排序6.归并排序总结 前言 排序在我们的生活当中无处不在&#xff0c;当然&#xff0c;它在计算机程序当中也是一种很重要的操…

【C语言】青蛙跳台阶 —— 详解

一、问题描述 跳台阶_牛客题霸_牛客网 (nowcoder.com) LCR 127. 跳跃训练 - 力扣&#xff08;LeetCode&#xff09; 二、解题思路 1、当 n 1 时&#xff0c;一共只有一级台阶&#xff0c;那么显然青蛙这时就只有一种跳法 2、当 n 2 时&#xff0c;一共有两级台阶&#xff…

你写过的最蠢的代码是?——前端篇

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页: &#x1f405;&#x1f43e;猫头虎的博客&#x1f390;《面试题大全专栏》 &#x1f995; 文章图文并茂&#x1f996…

LLVM 插桩 LLVM IR PHI指令

今天在进行 LLVM 插桩时&#xff0c;遇到一个神奇的报错 PHI nodes not grouped at top of basic block!%12 phi i32 [ %.pre, %if.then15 ], [ %argc, %maybe_close_fd_mask.exit ], !dbg !381 label %if.end19 PHI nodes not grouped at top of basic block!%18 phi i32 […

线程的状态与转换,组织与控制

进程和线程分析极其相似。见个人博客&#xff1a;进程的状态与转换以及组织方式 1.线程的状态与转换 2.线程的组织与控制 1.线程控制块&#xff08;TCB&#xff09; 2.线程表

运行中的代码,看不太懂的地方,可以实时查看运行值,以做进一步的判断

运行中的代码&#xff0c;看不太懂的地方&#xff0c;可以实时查看运行值&#xff0c;以做进一步的判断 运行中的代码&#xff0c;看不太懂的地方&#xff0c;可以实时查看运行值&#xff0c;以做进一步的判断 运行中的代码&#xff0c;看不太懂的地方&#xff0c;可以实时查看…

OpenCV 14(角点特征Harris和Shi-Tomasi)

一、角点 角点是图像很重要的特征,对图像图形的理解和分析有很重要的作用。角点在三维场景重建运动估计&#xff0c;目标跟踪、目标识别、图像配准与匹配等计算机视觉领域起着非常重要的作用。在现实世界中&#xff0c;角点对应于物体的拐角&#xff0c;道路的十字路口、丁字路…

机器学习 不均衡数据采样方法:imblearn 库的使用

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

【Hello Linux】多路转接之 epoll

本篇博客介绍&#xff1a; 多路转接之epoll 多路转接之epoll 初识epollepoll相关系统调用epoll的工作原理epoll服务器编写成员变量构造函数 循环函数HandlerEvent函数epoll的优缺点 我们学习epoll分为四部分 快速理解部分概念 快速的看一下部分接口讲解epoll的工作原理手写epo…

找不到msvcr120.dll怎么办?电脑缺失msvcr120.dll的修复方法

msvcr120.dll 是 Microsoft Visual C Redistributable Package 中的一个动态链接库文件&#xff0c;它包含了 C 运行时库的一些功能。这个文件通常与 Visual C 2010 编译器一起使用&#xff0c;用于支持一些大型游戏和应用程序的运行。msvcr120.dll 文件的主要作用是提供 C 语言…

远程代码执行渗透测试—Server2128

远程代码执行渗透测试 任务环境说明&#xff1a; √ 服务器场景&#xff1a;Server2128&#xff08;开放链接&#xff09; √服务器场景操作系统&#xff1a;Windows √服务器用户名&#xff1a;Administrator密码&#xff1a;pssw0rd 1.找出靶机桌面上文件夹1中的文件RCEBac…

【AI视野·今日Robot 机器人论文速览 第四十六期】Tue, 3 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 3 Oct 2023 Totally 76 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Generalized Animal Imitator: Agile Locomotion with Versatile Motion Prior Authors Ruihan Yang, Zhuoqun Chen, Jianhan M…

RabbitMQ-网页使用消息队列

1.使用消息队列 几种模式 从最简单的开始 添加完新的虚拟机可以看到&#xff0c;当前admin用户的主机访问权限中新增的刚添加的环境 1.1查看交换机 交换机列表中自动新增了刚创建好的虚拟主机相关的预设交换机。一共7个。前面两个 direct类型的交换机&#xff0c;一个是…