代码随想录算法训练营第三十七天|01背包问题、分割等和子集

news2024/12/23 13:22:31

01背包问题

在这里插入图片描述

题目链接:46. 携带研究材料
文档讲解:代码随想录
状态:忘了

二维dp

问题1:为啥会想到i代表第几个物品,j代表容量变化?

动态规划中,每次决策都依赖于前一个状态的结果,在背包问题中每次取物品的操作都必须考虑当前背包容量是否足够。所以使用i代表第几个物品,j代表背包容量限定。而第i个物品取和不取直接影响到最大价值总和dp[i][j]。

因此,dp[i][j]可以表示为,在容量j的条件下,取第i个物品所能得到的最大价值总和。

动态转移方程:

每次状态转移,需要考虑当前背包容量是否足够容纳物品i:

  • 如果当前物品i的重量 weight[i] 大于当前背包的容量j,则显然无法将物品i放入背包,因此 dp[i][j] 应该等于 dp[i-1][j],即不拿当前物品i时的最优解。
  • 如果当前物品i的重量 weight[i] 小于等于当前背包的容量j,则可以尝试将物品i放入背包(不能保证一定能放下)。此时,考虑两种情况:
    • 不放入物品i,也就是物品i放不下,否则能放下的话肯定是放入物品i后总价值更高!即 dp[i][j]=dp[i-1][j],和上面的情况一样!
    • 放入物品i,能放下物品i,肯定是放入后价值更高,即dp[i][j] = dp[i-1][j - weight[i]] + value[i],其中 value[i] 是物品i的价值。

考虑到上述情况,所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

二维dp题解:

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        // 当背包的容量大于等于第一个物品的重量时,才会将取第一个物品时最大价值设为第一个物品的价值
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i - 1][j];
                } else {
                    /**
                     * 当前背包的容量不确定可以放下物品i
                     * 那么此时分两种情况:
                     *    1、放不下,所以不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
    //这种方法初始化时,初始化了一个(m+1)×(n+1)的二维数组,包含了额外的一行和一列,用来表示没有放入任何物品时的情况。
    public static void testWeightBagProblem2(int[] weight, int[] value, int bagSize) {

        // 创建dp数组
        int m = weight.length;  // 获取物品的数量
        int n = bagSize;
        int[][] dp = new int[m + 1][n + 1];  // 创建动态规划数组,行表示物品数量,列表示背包容量

        // 填充dp数组
        for (int i = 1; i <= m; i++) {  // 遍历物品
            for (int j = 1; j <= n; j++) {  // 遍历背包容量
                if (j < weight[i - 1]) {  // 如果当前背包容量小于当前物品的重量,则无法装入该物品
                    dp[i][j] = dp[i - 1][j];  // 当前最优解等于上一个物品的最优解
                } else {  // 否则可以选择装入当前物品或者不装入当前物品,取两者中的最大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                    // 不装入当前物品:dp[i - 1][j]
                    // 装入当前物品:dp[i - 1][j - weight[i - 1]] + value[i - 1]
                    // value[i - 1] 表示当前物品的价值
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");  // 打印每个位置的最优值
            }
            System.out.println("\n");  // 换行
        }
    }

优化:

可以考虑用另一方式来定义dp[i][j]的含义,即dp[i][j] 表示考虑前i个物品,在背包容量为j时可以达到的最大价值。

那么dp[i][j]就可能从两种状态转换而来。

  • 第一种是当前物品i放不下,那么dp[i][j]=dp[i−1][j],也就是继承上一个状态的最优解
  • 第二种是可以放当前物品i,那么dp[i][j]=dp[i−1][j−weight[i−1]]+value[i],也就是在上一个状态的继承上加上当前物品i的价值

因此,可以的到递推公式:dp[i][j] = max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i]);

优化后题解:

// 使用了优化后的递推公式 dp[i][j] = max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
public static void testWeightBagProblem3(int[] weight, int[] value, int bagSize) {
    int length = weight.length;
    // 创建二维数组dp,dp[i][j]表示考虑前i个物品,在背包容量为j时的最大价值
    int[][] dp = new int[length + 1][bagSize + 1];
    
    // 遍历每个物品
    for (int i = 1; i <= length; i++) {
        // 遍历每个背包容量
        for (int j = 1; j <= bagSize; j++) {
            // 先假设不选第i个物品,继承上一个状态的最优解
            dp[i][j] = dp[i - 1][j];
            
            // 判断如果当前背包容量能够容纳第i个物品
            if (weight[i - 1] <= j) {
                // 考虑选择第i个物品后的最优解,这里要注意value[i - 1])是第i个物品的价值,因为第一行和第一列用0填充了,但是value数组是从索引0开始有意义的。
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
            }
        }
    }
}

一维dp:

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);也就是上面优化后的代码。
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。此时,dp[j]的状态要么是上次取完物品i-1的状态,要么是加入物品i的状态。

 在取物品0的时候,dp[j]会进行第一轮更新[0  15  15  15	 15]
 在取物品1的时候,dp[j]会进行第二轮更新[0  15	 15	 20	 35]
 在取物品2的时候,dp[j]会进行第三轮更新[0  15	 15  20	 35]

所以递推公式为,dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  • 左边的 dp[j]:表示在更新当前容量 j 时,新的 dp[j] 值。

  • 右边的 dp[j]:表示背包装不下物品i,所以继承上次容量为 j 时取完物品i-1的最大价值,即在未考虑物品 i 的情况下的最大价值。

  • 右边的 dp[j - weight[i-1]] + value[i]:表示在当前更新之前,容量为 j - weight[i-1] 时的最大价值,加上当前物品 i 的价值。

为什么需要从后向前遍历?

在使用一维数组 dp 时,从后向前遍历容量 j 是为了避免在同一轮次中使用已经更新的值。这保证了每个物品 i 在更新时只被计算一次,不会重复使用。

举个例子:
物品 1: 重量 2,价值 3
物品 2: 重量 3,价值 4
背包容量为 5。

从前向后遍历:
我们从前向后遍历容量 j 来更新 dp 数组。看看会发生什么情况。

遍历第一个物品(重量 2,价值 3):
j = 2:
dp[2] = max(dp[2], dp[2 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 3, 0, 0, 0]

j = 3:
dp[3] = max(dp[3], dp[3 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 3, 3, 0, 0]

j = 4:
dp[4] = max(dp[4], dp[4 - 2] + 3) = max(0, 3 + 3) = 6
更新后 dp = [0, 0, 3, 3, 6, 0]
从这里开始就出现问题了,求dp[4]的时候使用了更新后的dp[2]的值。

j = 5:
dp[5] = max(dp[5], dp[5 - 2] + 3) = max(0, 3 + 3) = 6
更新后 dp = [0, 0, 3, 3, 6, 6],求dp[5]的时候使用了更新后的dp[3]的值。

遍历第二个物品。。。。(略)

从后向前遍历:
遍历第一个物品(重量 2,价值 3):
j = 5:
dp[5] = max(dp[5], dp[5 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 0, 0, 0, 3],这里使用的dp[2]是还没更新的值。
…(略)

一维dp代码:

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
        int[] dp = new int[bagSize + 1];
        for (int i = 0; i < weight.length; i++) {
            for (int j = bagSize; j >= weight[i]; j--) {
                //因为i在更新,所以max中的dp[j]都是上一层中的dp[j],所以隐式地实现了"dp[i - 1]一层拷贝到dp[i]"
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);//max中的dp[j]是取上一个物品时对应容量j的最大价值
            }
        }
        for (int j = 0; j <= bagSize; j++) {
            System.out.print(dp[j] + "\t");
        }
    }

416. 分割等和子集

在这里插入图片描述

题目链接:416. 分割等和子集
文档讲解:代码随想录
状态:感觉像碰运气做出来的。。

思路:

第一步读题:分割成两个子集,使得两个子集的元素和相等,那么可以考虑先求和再除以2,得到目标值。对于nums中的数字,尝试不同的取值求和,只要得到和为target说明一定可以分成两个和相等的子集。所以可以考虑使用背包解题。

第二步判断背包类型:

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

以上分析完,我们就可以套用01背包,来解决这个问题了。

第三步:动规五部曲分析。

  • dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。在本题中就是取不同的值求得最大和dp[j]。本题中如果dp[j]=j就是满足条件了。
  • 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
  • 初始化,取第一个数字前,dp[j]都为0
  • 确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
  • 举例推导dp数组:如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j

题解:

// 一维dp实现
public boolean canPartition(int[] nums) {
    int sum = 0;
    // 计算数组总和
    for (int num : nums) {
        sum += num;
    }
    // 如果总和是奇数,不可能分成两个相等的子集
    if (sum % 2 == 1) {
        return false;
    }
    // 目标值是总和的一半
    int target = sum / 2;
    // 创建一维dp数组,dp[j]表示是否存在子集和为j
    int[] dp = new int[target + 1];
    // 遍历所有数字
    for (int i = 0; i < nums.length; i++) {
        // 倒序遍历所有可能的和
        for (int j = target; j > 0; j--) {
            // 如果当前数字小于等于目标和,更新dp数组
            if (nums[i] <= j) { // 刚开始没注意到这里, 其实最好写在for循环的判断条件中, 因为使用的数字肯定不能大于目标和
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        // 剪枝,只要有满足条件即可提前退出
        if (dp[target] == target) {
            return true;
        }
    }
    // 检查是否可以找到和为target的子集
    return dp[target] == target;
}

二维dp题解:

// 二维dp实现
public boolean canPartition(int[] nums) {
    int sum = 0;
    // 计算数组总和
    for (int num : nums) {
        sum += num;
    }
    // 如果总和是奇数,不可能分成两个相等的子集
    if (sum % 2 == 1) {
        return false;
    }
    // 目标值是总和的一半
    int target = sum / 2;
    // 创建二维dp数组,dp[i][j]表示前i个数能否组成和为j
    int[][] dp = new int[nums.length + 1][target + 1];
    // 遍历所有数字
    for (int i = 1; i <= nums.length; i++) {
        // 遍历所有可能的和
        for (int j = 1; j <= target; j++) {
            if (nums[i - 1] > j) {
                // 如果当前数字大于目标和,不能选当前数字,继承上一个状态的结果
                dp[i][j] = dp[i - 1][j];
            } else {
                // 否则,可以选择或者不选择当前数字,取两者的最大值
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i - 1]] + nums[i - 1]);
            }
        }
    }
    // 检查是否可以找到和为target的子集
    return dp[nums.length][target] == target;
}

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

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

相关文章

创新实训(十三) 项目开发——实现用户终止对话功能

思路分析&#xff1a; 如何实现用户终止AI正在进行的回答&#xff1f; 分析实现思路如下&#xff1a; 首先是在用户点击发送后&#xff0c;切换终止对话&#xff0c;点击后大模型终止对话&#xff0c;停止sse&#xff0c;不再接收后端的消息。同时因为对话记录存入数据库是后…

MySQL84 -- ERROR 1524 (HY000): Plugin ‘msql_native_password‘ is not loaded.

【问题描述】 MySQL 8.4版本&#xff0c;配置用户使用mysql_native_password认证插件验证用户身份&#xff0c;报错&#xff1a; 【解决方法】&#xff08;Windows, MySQL 8.4) 1、修改MySQL配置文件my.ini&#xff0c;在[mysqld]段添加mysql_native_passwordON。 2、管理员…

FastAPI教程I

本文参考FastAPI教程https://fastapi.tiangolo.com/zh/tutorial 第一步 import uvicorn from fastapi import FastAPIapp FastAPI()app.get("/") async def root():return {"message": "Hello World"}if __name__ __main__:uvicorn.run(&quo…

计算机体系结构 量化研究方法

在第一章中看到关于微处理器中dynamic energy 和 dynamic power的定义觉得有些奇怪&#xff0c;特别记录一下。 上面的定义是取决于上下文的&#xff1a;动态能量可以理解为在一个时钟周期内&#xff0c;由电容充放电消耗的能量总和&#xff0c;而动态功率则是这种能量消耗在单…

运算放大器输出相位反转和输入过压保护

运算放大器输出电压相位反转 本教程讨论两个与运算放大器相关的话题&#xff1a;输出相位反转和输入过压保护。 超过输入共模电压(CM)范围时&#xff0c;某些运算放大器会发生输出电压相位反转问题。其原因通常是运算放大器的一个内部级不再具有足够的偏置电压而关闭&#xff…

面试-Redis常见场景

1.从海量的数据里查询某一固定前缀的key (1)keys pattern指令&#xff1a; 摸清数据规模(很重要) (2)cursor&#xff1a;游标 Scan cursor [MATCH pattern] [COUNT count] 可以无阻塞的提取出指定模式的key列表 基于游标的迭代器&#xff0c;需要基于上一次的游标延续之前的迭…

【仿真建模-anylogic】开发规范

Author&#xff1a;赵志乾 Date&#xff1a;2024-06-28 Declaration&#xff1a;All Right Reserved&#xff01;&#xff01;&#xff01; 0. 说明 实际模型开发过程中&#xff0c;对遇到的问题进行总结归纳出以下开发规范&#xff0c;仅供参考&#xff01; 1. 强制性规范 1…

云动态摘要 2024-06-28

给您带来云厂商的最新动态&#xff0c;最新产品资讯和最新优惠更新。 最新优惠与活动 [新客专享]WeData 限时特惠 腾讯云 2024-06-21 数据分类分级管理&#xff0c;构建数据安全屏障 &#xff0c;仅需9.9元&#xff01; 云服务器ECS试用产品续用 阿里云 2024-04-14 云服务器…

使用QGIS进行研究区域制图实战

目录 前言 一、QGIS的版本和数据介绍 1、关于QGIS版本 2、需要准备的数据 二、准备制图 1、制作全国区位图 2、矢量和遥感信息的编辑 三、出图编辑 1、设置主题信息 2、打印布局制作 3、美化地图 总结 前言 俗话说“一图胜千言”&#xff0c;在地理信息的领域中&…

“AI+”时代,群核科技进化成了家居设计打工人理想的样子

6月&#xff0c;2024世界智能产业博览会上&#xff0c;人工智能大模型展团以“AI大模型驱动新质生产力”为主题&#xff0c;各家企业纷纷提到了基于不同行业场景的应用。 这透露出当前的行业发展趋势强调大模型落地核心行业&#xff0c;产生业务价值。其中&#xff0c;“AI图像…

车辆轨迹预测系列 (五):Argoverse API Forecasting Tutorial代码解析

车辆轨迹预测系列 (五)&#xff1a;Argoverse API Forecasting Tutorial代码解析 文章目录 车辆轨迹预测系列 (五)&#xff1a;Argoverse API Forecasting Tutorial代码解析一、argoverse.data_loading.argoverse_forecasting_loader二、argoverse.visualization.visualize_seq…

MTK8786/MT8786芯片性能参数_规格书_datasheet

联发科MT8786(MTK8786)处理器采用了台积电12纳米FinFET技术&#xff0c;由2个Cortex A75核心和6个Cortex A55核心组成&#xff0c;其中大核A75主频为2.0GHz&#xff0c;GPU采用了ARM Mali-G52。MTK8786芯片设计极大地提升了智能设备的性能和安全等级。 MT8786采用了安全的ISP设…

如何将 ONLYOFFICE 文档 Linux 版更新到 v8.1

本指南将向您展示如何将 ONLYOFFICE 文档 Linux 版本更新到最新 8.1 版本。 ONLYOFFICE 文档是什么 ONLYOFFICE 文档是一个功能强大的文档编辑器&#xff0c;支持处理文本文档、电子表格、演示文稿、可填写表单、PDF 和电子书&#xff0c;可多人在线协作&#xff0c;支持 AI 集…

【2024大语言模型必知】做RAG时为什么要使用滑动窗口?句子窗口检索(Sentence Window Retrieval)是什么?

目录 1. 传统的向量检索方法&#xff0c;使用整个文档检索&#xff0c;为什么不行&#xff1f; 2.句子滑动窗口检索&#xff08;Sentence Window Retrieval&#xff09;工作原理 3.句子滑动窗口检索&#xff08;Sentence Window Retrieval&#xff09;的优点 1. 传统的向量检…

【高考志愿】集成电路科学与工程

目录 一、专业概述 二、课程设置 三、就业前景 四、适合人群 五、院校推荐 六、集成电路科学与工程专业排名 一、专业概述 集成电路科学与工程&#xff0c;这一新兴且引人注目的交叉学科&#xff0c;正在逐渐崭露头角。它集合了电子工程、计算机科学、材料科学等多个领域的…

ASUS/华硕天选5 FX607J系列 原厂Windows11系统

安装后恢复到您开箱的体验界面&#xff0c;带原机所有驱动和软件&#xff0c;包括myasus mcafee office 奥创等。 最适合您电脑的系统&#xff0c;经厂家手调试最佳状态&#xff0c;性能与功耗直接拉满&#xff0c;体验最原汁原味的系统。 原厂系统下载网址&#xff1a;http:…

c语言入门

c语言入门 C语言一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好&#xff0c;可以用来开发应用软件、驱动、操作系统等。C语言也是其它众多高级语言的鼻祖语言&#xff0c;所以说学习C语言是进入编程…

AI大模型在短视频处理和剪辑中的应用

文章目录 &#x1f680;一、背景&#x1f680;二、视频处理方向 &#x1f50e;2.1 视频分析与标注&#x1f50e;2.2 视频剪辑与特效&#x1f50e;2.3 视频内容生成与故事叙述&#x1f50e;2.4 视频后期处理与发布 &#x1f680;三、AI视频处理书籍推荐 &#x1f50e;3.1 知识要…

Qt自定义信号

1.Teacher类下定义信号signals: Student类下定义槽函数&#xff1a; Teacher.h #pragma once#include <QObject>class Teacher : public QObject {Q_OBJECTpublic:Teacher(QObject *parent);~Teacher(); signals:void Ask(); //老师向学生提问void Ask(QString str);…

@ComponentScan注解在Spring的作用

ComponentScan注解的作用是什么&#xff1f; 告知Spring扫描那些包下的类&#xff0c;可以配置includeFilters&#xff0c;excludeFilters&#xff0c;过滤某些类&#xff0c;更多内容可以具体看下此注解文件。 Spring是如何通过这个注解将类注入到Ioc容器中的&#xff1f; 请…