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

文章目录

  • 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.mzph.cn/news/93699.shtml

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

相关文章

【CFD小工坊】浅水方程的离散及求解方法

【CFD小工坊】浅水方程的离散及求解方法 前言基于有限体积法的方程离散界面通量与源项计算干-湿网格的处理数值离散的稳定性条件参考文献 前言 我们模型的控制方程&#xff0c;即浅水方程组的表达式如下&#xff1a; ∂ U ∂ t ∂ E ( U ) ∂ x ∂ G ( U ) ∂ y S ( U ) U…

C++list模拟实现

list模拟实现 1.链表结点2.类模板基本框架3.构造4.插入普通迭代器实现4.1尾插4.2普通迭代器实现4.3对比list和vector的iterator4.4迭代器的价值4.5insert4.6尾插头插复用写法 5.删除erase5.1erase5.2尾删头删复用写法 6.析构emptysizeclear6.1clear6.2size6.3 empty6.4 析构 7.…

<C++>类和对象-下

目录 一、构造函数的初始化 1. 构造函数体赋值 2. 初始化列表 2.1 概念 2.2 隐式类型转换式构造 2.3 explicit关键字 二、static静态成员 1. 概念 2. 特性 三、友元 1. 友元函数 2.友元类 四、内部类 1. 概念 五、匿名对象 1. const引用匿名对象 2. 匿名对象的隐式类型转换 总…

C++左右值及引用

1 左值和右值 简单记法&#xff1a;能取地址的是左值&#xff0c;不能取地址的是右值 右值一般是常量 例&#xff1a; i 是右值&#xff0c;因为先把 i 赋值给临时变量&#xff0c;临时变量在1&#xff0c;而临时变量是将亡值&#xff0c;&i取地址会报错 i是左值&#xf…

获取网卡上的IP、网关及DNS信息,获取最佳路由,遍历路由表中的条目(附源码)

VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&a…

Oracle高级

Oracle数据库高级技术与最佳实践 Oracle数据库是全球领先的关系型数据库管理系统&#xff0c;广泛用于企业级应用程序和数据管理。在这篇博客中&#xff0c;我们将深入探讨Oracle数据库的高级技术和最佳实践&#xff0c;帮助数据库管理员和开发人员更好地利用其强大功能。 1. …

Linux进程控制

文章目录 前言一、进程创建1、fork函数2、写时拷贝3、子进程从哪里开始执行父进程代码 二、进程终止1、进程终止时&#xff0c;操作系统做了什么2、进程终止的常见方式2.1 main函数退出码 3、在代码中终止进程3.1 使用return语句终止进程3.2 使用exit函数终止进程3.3 使用_exit…

c#设计模式-结构型模式 之 组合模式

&#x1f680;简介 组合模式又名部分整体模式&#xff0c;是一种 结构型设计模式 &#xff0c;是用于把一组相似的对象当作一个 单一的对象 。组合模式 依据树形结构来组合对象 &#xff0c;用来表示部分以及整体层&#xff0c;它可以让你将对象组合成树形结构&#xff0c;并且…

leetCode 45.跳跃游戏 II 贪心算法

45. 跳跃游戏 II - 力扣&#xff08;LeetCode&#xff09; 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 &…

大语言模型之十四-PEFT的LoRA

在《大语言模型之七- Llama-2单GPU微调SFT》和《大语言模型之十三 LLama2中文推理》中我们都提到了LoRA&#xff08;低秩分解&#xff09;方法&#xff0c;之所以用低秩分解进行参数的优化的原因是为了减少计算资源。 我们以《大语言模型之四-LlaMA-2从模型到应用》一文中的图…

已解决 Bug——IndexError: index 3 is out of bounds for axis 0 with size 3问题

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

自然语言处理状况简介

一、说明 自然语言处理已经进入大模型时代&#xff0c;然而从业人员必须了解整个知识体系、发展过程、知识结构&#xff0c;应用范围等一系列知识。本篇将报道此类概况。 二、自然语言处理简介 自然语言处理&#xff0c;或简称NLP&#xff0c;是处理和转换文本的计算机科学学科…

格点数据可视化(美国站点的日降雨数据)

获取美国站点的日降雨量的格点数据&#xff0c;并且可视化 导入模块 from datetime import datetime, timedelta from urllib.request import urlopenimport cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.colors as mcolors import matplotli…

JAVA学习(2)-全网最详细~

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

选择排序算法:简单但有效的排序方法

在计算机科学中&#xff0c;排序算法是基础且重要的主题之一。选择排序&#xff08;Selection Sort&#xff09;是其中一个简单但非常有用的排序算法。本文将详细介绍选择排序的原理和步骤&#xff0c;并提供Java语言的实现示例。 选择排序的原理 选择排序的核心思想是不断地从…

CCF CSP认证 历年题目自练 Day20

题目一 试题编号&#xff1a; 201903-1 试题名称&#xff1a; 小中大 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 问题描述&#xff1a; 题目分析&#xff08;个人理解&#xff09; 常规题目&#xff0c;先看输入&#xff0c;第一行输入n表示有多少数字&am…

axb_2019_brop64

axb_2019_brop64 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)64位&#xff0c;只开了NX __int64 repeater() {size_t v1; // raxchar s[208]; // [rsp0h] [rbp-D0h] BYREFprintf("…

认识PostgreSQL

深入认识PostgreSQL&#xff1a;开源世界的强大数据库 在当今数字化时代&#xff0c;数据是组织的最宝贵资源之一。数据库管理系统&#xff08;DBMS&#xff09;扮演着关键角色&#xff0c;帮助企业存储、管理和分析数据。PostgreSQL&#xff0c;作为一款开源的高级关系型数据库…

在vue3中使用vite-svg-loader插件

vite-svg-loader插件可以让我们像使用vue组件那样使用svg图&#xff0c;使用起来超级方便。 安装 npm install vite-svg-loader --save-dev使用 import svgLoader from vite-svg-loaderexport default defineConfig({plugins: [vue(), svgLoader()] })组件里使用 在路径后加…

vue-引入使用main.js全局常量

common.js 命名什么都可以&#xff0c;用来定义常量的 定义了之后使用export让此暴露出去 const QRaddress http://localhost:9875export{QRaddress, } main.js //引入刚刚的js import {QRaddress} from /config/common.js挂载 Vue.prototype.$QRaddress QRaddress使用 …