动态规划-----背包类问题(0-1背包与完全背包)详解

news2024/11/25 20:36:34

目录

什么是背包问题?

动态规划问题的一般解决办法:

0-1背包问题:

0 - 1背包类问题  分割等和子集: 

完全背包问题: 

完全背包类问题 零钱兑换II:


什么是背包问题?

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkle和Hellman提出的。

动态规划问题的一般解决办法:

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤:

  • 🧐 步骤一:定义dp数组元素的含义
  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)
  • 🧐第三步骤:找出初始值(base case)

接下来的题目我们会按照这三个步骤来解释说明

前言:本文包含动态规划中的经典背包问题,有关背包问题的描述如下:

在动态规划中,背包问题是一个经典的优化问题,它可以分为0-1背包问题和完全背包问题两种类型。下面我们就来看看这两个问题:

0-1背包问题:

问题描述:

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i]。现在让你用这个背包装物品,每个物品只能用一次,在不超过被包容量的前提下,最多能装的价值是多少?

  • 🧐 步骤一:定义dp数组元素的含义:

由于状态有两个,就是「背包的容量」和「可选择的物品」,这里我们就需要用到一个二维的dp

数组,如下为dp数组的定义:

🦉🦉🦉dp[i][w] 的定义如下:对于前 i 个物品,当前背包的容量为 w,这种情况下可以装的最大价值是 dp[i][w]

  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)

1.如果你没有把这第 i 个物品装入背包,那么很显然,最大价值 dp[i][w] 应该等于 dp[i-1][w],继承之前的结果(翻译一下就是不装入第i个物品,相当于对前 i - 1 个物品进行选择,对应此时的背包容量w)。即此时的状态转移方程是:dp[ i ][ w ] = dp[ i - 1 ][ w ]

2.如果你把这第 i 个物品装入了背包,此时背包剩余容量为 w - wt[ i - 1 ](wt数组下标是从0开始的, wt[ i - 1 ] 相当于第 i 个物品的重量,val 也一样)

则此时的状态转移方程是:dp[ i ][ w ] = dp[ i - 1 ][ w - wt[ i - 1] ] + val[ i - 1 ]

  • 🧐第三步骤:找出初始值(base case):

这题的base case 相对简单,当物品个数为0或则背包当前容量为0时,dp[ i ][ w ] 都等于0

按照上述的状态转移方程,我们可以填出对应dp表格(以图中的例子为例):

有了上述铺垫后,动态规划的代码就很好实现了,具体代码如下:

int knapsack(int W, int N, int[] wt, int[] val) {
    assert N == wt.length;
    // base case 已初始化,数组自动全部初始化为0
    int[][] dp = new int[N + 1][W + 1];
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i - 1] < 0) {
                // 这种情况下只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = Math.max(
                    dp[i - 1][w - wt[i-1]] + val[i-1], 
                    dp[i - 1][w]
                );
            }
        }
    }
    
    return dp[N][W];
}

有了上面的一定了解后,我们来看看0 - 1背包的类似题:

0 - 1背包类问题  分割等和子集: 

看一下力扣第 416 题「分割等和子集open in new window」:

题目描述:输入一个只包含正整数的非空数组 nums,请你写一个算法,判断这个数组是否可以被分割成两个子集,使得两个子集的元素和相等。对应函数签名如下:

我们可以将这个问题转化为0 - 1背包问题,具体做法:

这个问题相当于给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?按照上述解题思路就是:

  • 🧐 步骤一:定义dp数组元素的含义:

dp[i][j] = x 表示,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满

  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)

与上面类似,这里就直接给出来了:

1.不把这第 i 个物品装入背包:dp[ i ][ j ] = dp[ i - 1 ][ j ]

2.把这第i个物品装入背包:dp[ i ][ j ] = dp[ i - 1][ j - nums[ i - 1 ] ]

  • 🧐第三步骤:找出初始值(base case):

当背包容量为0时(sum / 2= 0)这时无论物体有多少个都可以满足条件,就是什么都不装嘛

ok,接下来看完整代码:

boolean canPartition(int[] nums) {
    int sum = 0;
    for (int num : nums) sum += num;
    // 和为奇数时,不可能划分成两个和相等的集合
    if (sum % 2 != 0) return false;
    int n = nums.length;
    sum = sum / 2;
    boolean[][] dp = new boolean[n + 1][sum + 1];
    // base case
    for (int i = 0; i <= n; i++)
        dp[i][0] = true;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= sum; j++) {
            if (j - nums[i - 1] < 0) {
                // 背包容量不足,不能装入第 i 个物品
                dp[i][j] = dp[i - 1][j];
            } else {
                // 装入或不装入背包
                dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
    }
    return dp[n][sum];
}

完全背包问题: 

完全背包问题与0-1背包问题类似,但不同之处在于每个物品可以选择放入背包多次(数量无限),即每个物品的选择是一个无限的选择。我们给出对应的解题方法:

  • 🧐 步骤一:定义dp数组元素的含义:

若只使用前 i 个物品(可以重复使用),当背包容量为 j 时,能装入背包的最大价值为dp[i][w] 

  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)

1.不将第i个物品装入背包,此时:dp[ i ][ w ] = dp[ i - 1 ][ w ]

2.将第i个物品装入背包,此时:dp[ i ][ w ] = dp[ i ][ w -wt[ i - 1] ] + val[ i - 1 ] 

  • 🧐第三步骤:找出初始值(base case):

这题与0 - 1的bas背包的base case 一致,当物品个数为0或者背包当前容量为0时,dp[ i ][ w ] 都等于0

对应的动态规划代码为:

int fullBackpack(int W, int N, int[] wt, int[] val) {
    assert N == wt.length;
    // base case 已初始化,数组自动全部初始化为0
    int[][] dp = new int[N + 1][W + 1];
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i - 1] < 0) {
                // 这种情况下只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = Math.max(
                    dp[i][w - wt[i-1]] + val[i-1], 
                    dp[i - 1][w]
                );
            }
        }
    }
    
    return dp[N][W];
}

完全背包类问题 零钱兑换II:

这是力扣第 518 题「零钱兑换 IIopen in new window」,题目描述:

给定不同面额的硬币 coins 和一个总金额 amount,写一个函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。对应函数签名如下:

我们可以把这个问题转化为完全背包问题的描述形式

有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i]每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?按照动态规划三步走:

  • 🧐 步骤一:定义dp数组元素的含义:

dp[i][j] 的定义如下:若只使用前 i 个物品(可以重复使用),当背包容量为 j 时,有 dp[i][j] 种方法可以装满背包。

  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)

1.如果你不把这第 i 个物品装入背包,也就是说你不使用 coins[i-1] 这个面值的硬币,那么凑出面额 j 的方法数 dp[i][j] 应该等于 dp[i-1][j],继承之前的结果

即状态转移方程为:dp[ i ][ j ] = dp[ i -1 ][ j ]

2.如果你把这第 i 个物品装入了背包,也就是说你使用 coins[i-1] 这个面值的硬币,那么 dp[i][j] 应该等于 dp[i][j-coins[i-1]]

即状态转移方程为:dp[ i ][ j ] = dp[ i ][ j - coins[ i - 1] ]

  • 🧐第三步骤:找出初始值(base case):

base case 为 dp[0][..] = 0, dp[..][0] = 1i = 0 代表不使用任何硬币面值,这种情况下显然无法凑出任何金额;j = 0 代表需要凑出的目标金额为 0,那么什么都不做就是唯一的一种凑法。

写出动态规划代码:

int change(int amount, int[] coins) {
    int n = coins.length;
    int[][] dp = int[n + 1][amount + 1];
    // base case
    for (int i = 0; i <= n; i++) 
        dp[i][0] = 1;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= amount; j++)
           //装入背包或者不装入,加起来
            if (j - coins[i-1] >= 0)
                dp[i][j] = dp[i - 1][j] 
                         + dp[i][j - coins[i-1]];
            else // < 0 只能选择不装入背包
                dp[i][j] = dp[i - 1][j];
    }
    return dp[n][amount];
}

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固自己的知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+收藏+关注,你们的鼓励是我创作的最大动力!

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

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

相关文章

jsp中设置动态时间

第一步 在head中写入meta <head><meta charset"UTF-8" http-equiv"Refresh" content"1"> </head> 第二步在head中写入函数 <head><meta charset"UTF-8" http-equiv"Refresh" content"…

Mac m1 Flink的HelloWorld

首先在官方下载Downloads | Apache Flink 下载好压缩包后解压&#xff0c;得到Flink文件夹 进入&#xff1a;cd flink-1.19.0 ls 查看里面的文件&#xff1a; 执行启动集群 ./bin/start-cluster.sh 输出显示它已经成功地启动了集群&#xff0c;并且正在启动 standalonesessio…

基于YOLOV8+Pyqt5光伏太阳能电池板目标检测系统

1、YOLOV8算法 YOLOv8 是当前效果较好的目标检测 算法&#xff0c;它的核心网络来源于 DarkNet-53&#xff0c;该网络初次在 YOLOv3[11] 中被引入&#xff0c;并深受 ResNet[12] 的影响。DarkNet-53 使用了残差机制&#xff0c;并连续添加了卷积模块来加强其功能性。 这 53 层…

AI 音乐的 “ChatGPT“ 时刻,SunoV3简介和升级教程

一句话总结 Suno AI音乐平台发布了V3版本&#xff0c;标志着AI音乐创作领域的一个重要进步&#xff0c;类似于ChatGPT在文本生成领域的影响。 关键信息点 Suno AI是专注于生成式AI音乐的平台&#xff0c;最新发布的V3版本在音质、咬字和节奏编排上有显著提升。V3版本的AI音乐…

集成百兆,千兆,万兆网络变压器等电子元器件的RJ45 Jack连接器在屏显控制系统中的应用

Hqst华轩盛(石门盈盛)电子导读&#xff1a;集成百兆&#xff0c;千兆&#xff0c;万兆网络变压器等电子元器件的RJ45 Jack连接器在屏显控制系统中的应用 一 ﹑集成百兆&#xff0c;千兆&#xff0c;万兆网络变压器等电子元器件的RJ45 Jack连接器在屏显控制系统中的应用前景 近年…

SQL Server 数据库常见提权总结

前面总结了linux和Windows的提权方式以及Mysql提权&#xff0c;这篇文章讲讲SQL Server数据库的提权。 目录 基础知识 权限判定 系统数据库 存储过程 常见系统存储过程 常见扩展存储过程 xp_cmdshell扩展存储过程提权 xp_dirtree写入文件提权 sp_oacreate提权 xp_re…

【吊打面试官系列】Redis篇 -Redis 如何做内存优化?

大家好&#xff0c;我是锋哥。今天分享关于 【Redis 如何做内存优化&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; Redis 如何做内存优化&#xff1f; 尽可能使用散列表&#xff08;hashes&#xff09;&#xff0c;散列表&#xff08;是说散列表里面存储的数…

[ESP32]:基于esp-modbus实现serial从机

[ESP32]&#xff1a;基于esp-modbus实现serial从机 开发环境&#xff1a; esp idf 5.1esp-modbus 1.0.13 使用如下指令添加组件&#xff0c;或者访问esp-modbus idf.py add-dependency "espressif/esp-modbus^1.0.13"1.mb_register_area_descriptor_t 对于slave…

PHP远程命令执行与代码执行原理利用与常见绕过总结

PHP远程命令执行与代码执行原理利用与常见绕过总结 远程命令执行 相较于SQL注入漏洞&#xff0c;远程命令执行更加少见。由于是直接执行系统命令&#xff0c;所以相较于前者此漏洞会更加危险&#xff1a; 攻击者通过远程命令执行漏洞可以直接掌控服务器攻击者可以通过存在此…

OSCP靶场--Internal

OSCP靶场–Internal 考点(CVE-2009-3103) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.216.40 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-31 07:00 EDT Nmap scan report for 192.168.216.40 Host is up…

【微服务框架】微服务简介

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

react ts 封装搜索条件

封装 import React, { ReactNode, useImperativeHandle, forwardRef } from react; import { Card, Form, Input, Button, Row, Col } from antd;interface Iprops {formItem: any,getParams: (params: any) > void,reset?: () > void | undefined,isButton?: boolean…

算法学习16:数论03(容斥原理、博弈论)

算法学习16&#xff1a;数论03&#xff08;容斥原理、博弈论&#xff09; 文章目录 算法学习16&#xff1a;数论03&#xff08;容斥原理、博弈论&#xff09;前言一、容斥原理&#xff1a;求多个集合的并集二、博弈论1.Nim游戏&#xff1a;2.集合N-im游戏 总结 前言 提示&#…

南京观海微电子---Vitis HLS设计流程介绍——Vitis HLS教程

1. 传统的FPGA设计流程 传统的RTL设计流程如下图所示&#xff1a; 传统的FPGA RTL设计流程主要是采用VHDL、VerilogHDL或System Verilog进行工程的开发&#xff0c;同时也是通过硬件描述语言来编写测试案例&#xff08;Test Bench&#xff09;对开发的工程进行仿真验证。 随后…

教你一键轻松领取腾讯云优惠券

随着云计算的普及&#xff0c;越来越多的企业和个人开始使用云服务。而在众多云服务提供商中&#xff0c;腾讯云凭借其优质的服务和丰富的产品赢得了广大用户的青睐。为了吸引更多的用户上云&#xff0c;腾讯云推出了优惠券活动&#xff0c;让用户在购买云服务时能够获得更多实…

如何使用Axure RP制作网页原型并结合IIS服务实现公网访问本地HTML网页

文章目录 前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4.2 启动website隧道4.3 获取公网URL地址4.4. 公网远程访问内网web站点4.5 配置固定二级子域名公网访问内网web站点4.5.1创建一条固定…

Go的数据结构与实现【Binary Search Tree】

介绍 本文用Go将实现二叉搜索树数据结构&#xff0c;以及常见的一些方法 二叉树 二叉树是一种递归数据结构&#xff0c;其中每个节点最多可以有两个子节点。 二叉树的一种常见类型是二叉搜索树&#xff0c;其中每个节点的值都大于或等于左子树中的节点值&#xff0c;并且小…

【JavaWeb】Day28.SpringBootWeb请求响应——请求(一)

前言&#xff1a; 我们在开发web程序时呢&#xff0c;定义了一个控制器类Controller&#xff0c;请求会被部署在Tomcat中的Controller接收&#xff0c;然后Controller再给浏览器一个响应。 而在请求响应的过程中是遵循HTTP协议的。 但是&#xff0c;在Tomcat这类Web服务器中&a…

redis学习-redis配置文件解读

目录 1.单位说明 2. include配置 3. network网络配置 3.1 bind绑定ip配置 3.2保护模式protected-mode配置 3.3端口号port配置​编辑 3.4超时断开连接timeout配置 4. general通用配置 4.1守护进程模式daemonize配置 4.2进程id存放文件pidfile配置 4.3日志级别loglevel配置 4.…

揭秘汽车制造神器:DeviceNET转Modbus TCP神操作

在现代汽车制造行业&#xff0c;汽车零部件的高效生产与精准控制是至关重要的。为了实现这一目标&#xff0c;上位机通过DeviceNET转Modbus TCP网关的技术应用越来越受到重视。这种技术不仅提高了生产线的自动化程度&#xff0c;而且确保了数据的准确传输和处理&#xff0c;为汽…