leetcode动态规划学习

news2025/1/12 10:05:19

0-1背包问题

参考:

【动态规划/背包问题】那就从 0-1 背包问题开始讲起吧 ...

内容是学习 宫水三叶的刷题日记 公众号专题内容时的笔记,为了方便个人复习整理到这里。建议大家关注该公众号,写的很清楚,有更多内容。

经典0-1背包

二维数组(空间复杂度为O(mn))

动态规划数组含义:

即第i行代表第i个物品(0表示第一个物品),第j列代表背包容量为j,背包容量从0开始,所以列数为容量+1。

(此部分图片来源:背包问题——01背包|完全背包-CSDN博客)

初始化:背包容量为0时最大价值为0,所以第0列全部为0(int数组初始化为0所以不用处理这个),第一行当容量小于w[0]时值为0,大于等于时值为w[i]

对每一个物品,都只有两种状态,选或者不选:

所以转移方程为:

(把c看成j就好了,懒得自己写公式了)

代码:

class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[][] dp = new int[N][C+1];
        // 先处理「考虑第一件物品」的情况
        for (int i = 0; i <= C; i++) {
            dp[0][i] = i >= v[0] ? w[0] : 0;
        }
        // 再处理「考虑其余物品」的情况
        for (int i = 1; i < N; i++) {
            for (int j = 0; j < C + 1; j++) {
                // 不选该物品
                int n = dp[i-1][j]; 
                // 选择该物品,前提「剩余容量」大于等于「物品体积」
                int y = j >= v[i] ? dp[i-1][j-v[i]] + w[i] : 0; 
                dp[i][j] = Math.max(n, y);
            }
        }
        return dp[N-1][C];
    }
}

滚动数组优化(空间复杂度为O(2*n))

根据「转移方程」,我们知道计算第 i行格子只需要第 i-1行中的某些值。

也就是计算「某一行」的时候只需要依赖「前一行」。

因此可以用一个只有两行的数组来存储中间结果,根据当前计算的行号是偶数还是奇数来交替使用第 0 行和第 1 行。

这样的空间优化方法称为「滚动数组」,我在 路径问题 第四讲 也曾与你分享过。

这种空间优化方法十分推荐,因为改动起来没有任何思维难度。

只需要将代表行的维度修改成 2,并将dp数组中所有使用行维度的地方从 i改成 i&1或者 i%2 即可(更建议使用 ,& 运算在不同 CPU 架构的机器上要比 % 运算稳定)。

代码:

class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[][] dp = new int[2][C+1];
        // 先处理「考虑第一件物品」的情况
        for (int i = 0; i <= C; i++) {
            dp[0][i] = i >= v[0] ? w[0] : 0;
        }
        // 再处理「考虑其余物品」的情况
        for (int i = 1; i < N; i++) {
            for (int j = 0; j < C + 1; j++) {
                // 不选该物品
                int n = dp[(i-1)&1][j]; 
                // 选择该物品,前提「剩余容量」大于等于「物品体积」
                int y = j >= v[i] ? dp[(i-1)&1][j-v[i]] + w[i] : 0; 
                dp[i&1][j] = Math.max(n, y);
            }
        }
        return dp[(N-1)&1][C];
    }
}

一维数组(空间复杂度为O(n))

再次观察我们的「转移方程」:

(把c看成j就好了,懒得自己写公式了)

不难发现当求解第i行格子的值时,不仅是只依赖第 i-1行,还明确只依赖第 i-1行的第j个格子和第j-v[i]个格子。

所以可以进一步将行这个维度优化掉,直接改为dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]),但是有一个问题就是我们原来是从左向右去遍历j的,如果从左向右的话那当我们改dp[j]的时候它左边的dp[j-v[i]]应该也已经被改过了,这时候对应的是i而不是i-1,所以我们要更改遍历顺序,j从右向左遍历,因为我们要取下标为j-v[i]的元素,所以循环控制条件为j>=v[i]

代码

class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[] dp = new int[C + 1];
        for (int i = 0; i < N; i++) {
            for (int j = C; j >= v[i]; j--) {
                // 不选该物品
                int n = dp[j]; 
                // 选择该物品
                int y = dp[j-v[i]] + w[i]; 
                dp[j] = Math.max(n, y);
            }
        }
        return dp[C];
    }
}

如何将原问题抽象为「01 背包」问题

例题1 分割等和子集

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

示例 1:

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

示例 2:

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

原问题可以看做,从nums中挑选N个元素使得元素总和等于所有元素和的一半。设所有和为sum,所有元素和一半为target。这个问题就可以看做是,背包容量为target,物品重量和价值都为nums[i]的一个背包问题,将最大能容纳的价值求解出来看是不是等于target,就等价于原数组能不能分割成两个子集。

二维数组:dp = new int[N][target+1],初始化第一行,所有容量小于nums[i]的为0,其余为nums[i],转移方程为dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i])。

优化一维数组:dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i])

代码:

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;

        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;

        // 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
        if (target * 2 != sum) return false;

        // 将「物品维度」取消
        int[] f = new int[target + 1];
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            // 将「容量维度」改成从大到小遍历
            for (int j = target; j >= 0; j--) {
                // 不选第 i 件物品
                int no = f[j];
                // 选第 i 件物品
                int yes = j >= t ? f[j-t] + t : 0;
                f[j] = Math.max(no, yes);
            }
        }
        // 如果最大价值等于 target,说明可以拆分成两个「等和子集」
        return f[target] == target;
    }
}

总结:

可以发现,本题的难点在于「对问题的抽象」,主要考察的是如何将原问题转换为一个「01 背包」问题。

事实上,无论是 DP 还是图论,对于特定问题,大多都有相应的模型或算法。

难是难在如何将问题转化为我们的模型。

至于如何培养自己的「问题抽象能力」?

首先通常需要我们积累一定的刷题量,并对「转换问题的关键点」做总结。

例如本题,一个转换「01 背包问题」的关键点是我们需要将「划分等和子集」的问题等效于「在某个数组中选若干个数,使得其总和为某个特定值」的问题。

间接求解到直接求解的转变:

但这道题到这里还有一个”小问题“。就是我们最后是通过「判断」来取得答案的。

通过判断取得的最大价值是否等于target来决定是否能划分出「等和子集」。

虽然说逻辑上完全成立,但总给我们一种「间接求解」的感觉。

造成这种「间接求解」的感觉,主要是因为我们没有对「01 背包」的「状态定义」和「初始化」做任何改动。

但事实上,我们是可以利用「01 背包」的思想进行「直接求解」的。

当我们与某个模型的「状态定义」进行了修改之后,除了考虑调整「转移方程」以外,还需要考虑修改「初始化」状态。

试考虑,我们创建的数组存储的是布尔类型,初始值都是false,这意味着无论我们怎么转移下去,都不可能产生一个true,最终所有的状态都仍然是false。换句话说,我们还需要一个有效值true来帮助整个过程能递推下去。

通常我们使用「首行」来初始化「有效值」。

将「物品编号」从 0 开始调整为从 1 开始。

原本我们的f[0][x]代表只考虑第一件物品、 f[1][x]代表考虑第一件和第二件物品;调整后我们的 f[0][x]代表不考虑任何物品、 f[1][x]代表只考虑第一件物品 ...

这种技巧本质上还是利用了「哨兵」的思想。

二维代码:

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;

        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;

        // 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
        if (target * 2 != sum) return false;

        // f[i][j] 代表考虑前 i 件物品,能否凑出价值「恰好」为 j 的方案
        boolean[][] f = new boolean[n+1][target+1];
        f[0][0] = true;
        for (int i = 1; i <= n; i++) {
            int t = nums[i-1];
            for (int j = 0; j <= target; j++) {
                // 不选该物品
                boolean no = f[i-1][j];
                // 选该物品
                boolean yes = j >= t ? f[i-1][j-t] : false;
                f[i][j] = no | yes;
            }
        }
        return f[n][target];
    }
}

一维代码:

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;

        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;

        // 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
        if (target * 2 != sum) return false;

        // 取消「物品维度」
        boolean[] f = new boolean[target+1];
        f[0] = true;
        for (int i = 1; i <= n; i++) {
            int t = nums[i-1];
            for (int j = target; j >= 0; j--) {
                // 不选该物品
                boolean no = f[j];
                // 选该物品
                boolean yes = j >= t ? f[j-t] : false;
                f[j] = no | yes;
            }
        }
        return f[target];
    }
}

例题2 最后一块石头的重量 II

有一堆石头,用整数数组 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

问题转化为:把一堆石头分成较小的A、较大的B两堆,求两堆石头重量差最小值。

进一步分析:要让差值小,两堆石头的重量都要接近sum/2;我们假设两堆分别为A,B,A<sum/2,B>sum/2,若A更接近sum/2,B也相应更接近sum/2,用动态规划求解A堆在背包数量为sum/2情况下能装的最大的石头价值,设其为max,那么B堆石头的价值就是sum-max,两个石头堆相撞以后剩余的重量就是B-A=sum-2*max

所以先用dp数组求解A堆,最后返回B-A值

代码

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for(int s:stones){
            sum += s;
        }
        int target = sum/2;
        int[] dp = new int[target+1];
        for(int i=0;i<stones.length;i++){
            for(int j=target;j>=stones[i];j--){
                dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
}

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

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

相关文章

Linux进程等待

Linux进程等待 一.什么是进程等待二.为什么要有进程等待三.怎么进行进程调用1.僵尸进程问题2.waitpid3.获取进程退出状态 一.什么是进程等待 通过系统调用wait和waitpid&#xff0c;来进行对子进程进行状态检测与回收功能。 二.为什么要有进程等待 1。之前讲过&#xff0c;子…

【第25例】IPD体系进阶:需求分析团队RAT

目录 简介 RAT CSDN学院相关内容推荐 作者简介 简介 RAT是英文Requirement Analysis Team英文首字母的简称,也即需求分析团队,每个产品线都需要设定对应的一个RAT的组织。 RAT主要负责产品领域内需求的分析活动,是RMT的支撑团队: 这个时候可以将RAT细化为PL-RAT团队,…

C语言--strcmp函数(介绍与自己实现)

strcmp函数&#xff1a;字符串比较函数。 用法&#xff1a;strcmp(str1,str2)。str1&#xff0c;str2可以是字符串常量或者字符串变量&#xff0c;返回值为整形。 str1小于str2,返回负值&#xff0c;str1等于str2,返回为0&#xff0c;str1大于str2,返回正值 那么两个字符串是…

Ubuntu环境下DOSBOX的配置

【步骤一】 先打开命令行&#xff0c;进入root模式&#xff0c;输入如下语句&#xff1a; sudo apt-get install dosbox 该语句的作用主要是安装dosbox 【步骤二】 安装完成之后&#xff0c;命令行输入dosbox 会进入dosbox页面&#xff1a; 【步骤三】 在你的主机中&…

腾讯云主机服务器什么时候最便宜?

腾讯云主机服务器价格对于许多用户来说是一个重要考虑因素。本文将探讨腾讯云主机服务器的最佳购买时期&#xff0c;并为大家提供一些建议&#xff0c;帮助大家以最优惠的价格购买腾讯云主机服务器。 首先&#xff0c;我们需要了解腾讯云服务器的优惠政策。一般来说&#xff0c…

Games104现代游戏引擎笔记 网络游戏进阶架构

Character Movement Replication 角色位移同步 玩家2的视角看玩家1的移动是起伏一截一截&#xff0c;并且滞后的 interpolation&#xff1a;内插值&#xff0c;在两个旧的但已知的状态计算 extrapolation&#xff1a;外插值&#xff0c;本质是预测 内插值&#xff1a;但网络随着…

用友U8SMSProxy -SQL注入漏洞

0x01 漏洞介绍 用友GRP-U8 R10政务管理软件是由用友政务公司基于云技术所推出的第十代政务产品。这款产品继承了用友R9、R9i、U8等行政事业版产品的各项优点&#xff0c;并融合了全国广大用户的最佳实践应用。它旨在为政府财政部门、社保部门、卫生部门、教育部门、民政部门、党…

phar反序列化

序列化和反序列化的区别&#xff1f; 可参考&#xff1a;【精选】什么是反序列化&#xff1f;反序列化的过程&#xff0c;原理-CSDN博客 通俗的说序列化将对象转化为了字符串&#xff0c;包含了对象的所有数据信息&#xff0c; 反序列化时再根据这些信息还原对象 序列化&…

0026Java程序设计-中学走读生信息管理系统设计与实现

文章目录 摘要**目录**系统设计开发环境 摘要 目前&#xff0c;中学走读生信息管理系统已经发展成为学校的学生走读管理工作中必不可少的一个组成部分&#xff0c;没有该系统&#xff0c;学生的日常工作就会变得繁琐、效率低下。在信息化的社会发展下&#xff0c;有必要建立一…

Three.js 基础纹理贴图

本文简介 带尬猴&#xff0c;我嗨德育处主任 尽管 Three.js 文档已经比较详细了&#xff0c;但对于刚接触 Three.js 的工友来说&#xff0c;最麻烦的还是不懂如何组合。Three.js 的功能实在太多了&#xff0c;初学者很容易被大量的新概念冲晕。 本文主要讲解入门 Three.js 必…

BES2700 蓝牙协议之RFCOMM通道使用方法

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?可加我微信hezkz17, 本群提供音频技术答疑服务 BES2700 RFCOMM通道使用方法 RFCOMM_CHANNEL_NUM 枚举定义了一系列的通道号码,并为每个通道号码指定了一个具体的名称。以下是其中一些通道的中文含义: RFCOMM_CHAN…

解决Windows出现找不到mfcm90u.dll无法打开软件程序的方法

今天&#xff0c;我非常荣幸能够在这里与大家分享关于mfc90u.dll丢失的5种解决方法。在我们日常使用电脑的过程中&#xff0c;可能会遇到一些软件或系统错误&#xff0c;其中之一就是mfc90u.dll丢失。那么&#xff0c;mfc90u.dll究竟是什么文件呢&#xff1f;接下来&#xff0c…

【代码随想录】算法训练计划02

1、977. 有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff1a;[0,1,9,16,100] 思路&#xff1a; 这题思路在于——双指针…

网工内推 | 国企,解决方案工程师,最高30k,有软考证书优先

01 中电信数智科技有限公司海南分公司 招聘岗位&#xff1a;解决方案经理&#xff08;ICT&#xff09; 职责描述&#xff1a; 1、负责调动前后端资源做好全省ICT业务的售前支撑服务工作。 2、根据实际项目需求&#xff0c;主动协同客户渠道开展ICT项目商机挖掘&#xff0c;促进…

点云处理【七】(点云配准)

点云处理 第一章 点云数据采集 1.点云配准 点云配准是将两个或多个点云数据集融合到一个统一的坐标系统中的过程。这通常是为了创建一个完整的模型或融合从不同视角采集的数据。 点云配准一般分为粗配准和精配准&#xff0c;粗配准指的是在两幅点云之间的变换完全未知的情况下…

JS加密/解密之那些不为人知的基础逻辑运算符

不多说&#xff0c;直接上干货 使用逻辑非运算符 ! 和双重逻辑非运算符 !!&#xff1a;例如 ![]、!![]、!0、!!0 和 !""、!!""。空字符串的转换&#xff1a;!"" 和 !!""。数组和对象的类型转换&#xff1a;[] []、[] - []、{} [] 和…

STM32中除零运算,为何程序不崩溃?

在 C 语言中&#xff0c;除零运算会导致异常吗&#xff1f; 在 C 语言中&#xff0c;当一个数除以零时&#xff0c;会导致除法运算错误&#xff0c;通常表现为“除以零”错误或被称为“浮点异常”&#xff08;floating-point exception&#xff09;。 对于整数除法&#xff0c…

YOLOv5源码中的参数超详细解析(3)— 训练部分(train.py)| 模型训练调参

前言:Hello大家好,我是小哥谈。YOLOv5项目代码中,train.py是用于模型训练的代码,是YOLOv5中最为核心的代码之一,而代码中的训练参数则是核心中的核心,只有学会了各种训练参数的真正含义,才能使用YOLOv5进行最基本的训练。🌈 前期回顾: YOLOv5源码中的参数超详细解析…

IC-705连接wfview

wfview是一款开源的主要针对ICOM的远程控制软件&#xff0c;可以通过USB或者无线控制电台&#xff0c;貌似还支持X6100。 IC-705支持WLAN功能&#xff0c;连接wfview非常方便。 IC-705的WLAN支持两种模式&#xff0c;一种是Station模式&#xff0c;可用于连接WI-FI路由器&#…

vue中使用xlsx插件导出多sheet excel实现方法

安装xlsx&#xff0c;一定要注意版本&#xff1a; npm i xlsx0.17.0 -S package.json&#xff1a; {"name": "hello-world","version": "0.1.0","private": true,"scripts": {"serve": "vue-c…