【算法 | 背包专题】01背包(解题思路+题单)

2024-04-07 17:04

本文主要是介绍【算法 | 背包专题】01背包(解题思路+题单),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

什么是背包问题?

背包问题是一种经典的组合优化问题,它的核心思想是在有限的资源(如背包的容量)下,如何选择物品以达到某种目标(如最大价值)的最优解。

背包问题可以分为几种类型,其中最常见的有:

  1. 0/1背包问题:每个物品只能选择放入或不放入背包,不能分割。
  2. 完全背包问题:每种物品可以选择无限个,但每件物品只能选择一次。
  3. 多重背包问题:每种物品可以选择多个,且没有数量限制。

解决方法也有很多,比如动态规划,回溯搜索、分支限界,贪心等。

背包问题在实际生活中有很多应用,如资源分配、项目投资组合、货物装载、课程安排等。通过解决背包问题,我们可以在有限的资源下做出最优的决策。

本节我们就来看01背包问题。

问题描述

01背包问题的描述如下:

  • 现在,我们有一个背包,它有一个固定的承载重量限制 W(背包容量有限)。
  • 同时,我们有一组物品,每个物品都有自己的重量weight[i]和价值value[i]
  • 我们需要从这组物品中选择一些物品放入背包,并且每件物品只能用一次
  • 问:在不超过背包承载重量的前提下,放入背包的物品总价值最大是多少

在01背包中,每个物品最多只能用一次。

问题求解

暴力

在这个经典的 01背包问题中,对于我们的决策,每一件物品只有两个状态:

  • 不选

使用回溯法,我们可以搜索出所有情况,但时间复杂度是 O ( 2 n ) O(2^n) O(2n) n n n表示物品数量),因此暴力的解法是通不过的,需要进一步优化。

对于01背包问题,最常用的求解方法是动态规划

dp表的含义

使用动态规划的解法,需要定义状态(明确dp表的含义)和状态转移方程,将问题分解为子问题,然后通过迭代的方式,从小问题开始,逐步解决大问题。

我们来看,在01背包问题中,状态是如何定义的:

  • dp[i][j]:在前i个物品中选取若干个,使得总重量不超过j的情况下,能够获得的最大价值。

明确好dp表的含义后,我们就可以进行状态转移的讨论以及编码。

状态转移

对于每个物品i以及当前的背包容量j,我们考虑两种情况(选或不选):

  • 如果不选取i个物品:
    • 那么背包中物品的总价值就是在前i-1个物品中选取若干个,使得总重量不超过j的情况下,能够获得的最大价值,
    • dp[i][j] = dp[i - 1][j]
  • 如果选取i个物品:
    • 背包中物品的总价值就是在前i-1个物品中选取若干个,使得总重量不超过j-weight[i]的情况下,能够获得的最大价值,再加上第i个物品的价值,
    • dp[i][j] = dp[i - 1][j - weight[i]] + value[i]

我们对每个物品的每种可能性进行考虑,从而找出在总重量不超过背包容量的前提下,能够获得的最大价值。这就是01背包问题的解题思路。

解题流程

定义好状态,以及转移方程后,我们就可以开始推了。从第1个物品开始,对于每个物品,遍历所有的背包容量,根据状态转移方程更新dp表。最后,dp[n][t]就是最大价值。

在使用动态规划时,初始化操作是很关键的一步。对于dp[0][j](没有物品时),无论背包容量是多少,最大价值都是0,因此初始化就都是0。

代码如下:

/*** 使用动态规划解决01背包问题* @param weight 物品的重量数组* @param value 物品的价值数组* @param W 背包的总容量* @return 能够获得的最大价值*/
public static int compute(int[] weight, int[] value, int W) {int n = value.length;	// 有n件物品// dp[i][j]表示在前i个物品中选取若干个,使得总重量不超过j的情况下,能够获得的最大价值int[][] dp = new int[n+1][W+1];// 遍历每一个物品for(int i = 1; i<=n; i++) {// 遍历每一种背包容量for(int j = 0; j<=W; j++) {// 不选取第i个物品dp[i][j] = dp[i-1][j];// 如果背包的剩余容量大于等于当前物品的重量,考虑选取第i个物品if(j >= weight[i]) {// 选取第i个物品,更新最大价值dp[i][j] = Math.max(dp[i][j], dp[i-1][j-weight[i]] + value[i]);}}}// 返回在前n个物品中选取若干个,使得总重量不超过W的情况下,能够获得的最大价值return dp[n][W];
}

空间压缩

在上述代码中,我们可以看到,每次更新dp[i][j]时,我们只用到了上一行的数据,即dp[i-1][j]dp[i-1][j-weight[i]]。这意味着我们并不需要保存所有的dp[i][j],只需要保存上一行的数据就足够了,因此,我们可以将二维dp表改进为一维,俗称空间压缩。

空间压缩的思路是,我们使用一个一维数组dp[j]来代替二维数组dp[i][j]dp[j]表示在当前考虑的物品中选取若干个,使得总重量不超过j的情况下,能够获得的最大价值。

需要注意的是,我们每次在更新dp[i][j]时,总是用到了上一行中的dp[i-1][j]dp[i-1][j-weight[i]],在二维表中可以形象理解为,我所处的位置,依赖于上方的格子,以及左上方的格子。因此,进行空间压缩更新dp[j]时,我们需要从后往前更新dp[j],这样可以逐渐更新当前的格子。

如果我们从前往后更新,那么在计算dp[j]时,dp[j-weights[i]]可能已经被更新过了,它表示的是当前行的状态,而不是上一行的状态。而我们需要的是上一行的状态,因此我们必须从后往前更新。

下面是空间压缩后的代码:

/*** 使用动态规划解决01背包问题(空间压缩版本)* @param weight 物品的重量数组* @param value 物品的价值数组* @param W 背包的总容量* @return 能够获得的最大价值*/
public static int compute(int[] weight, int[] value, int W) {int n = value.length;	// 有n件物品// dp[j]表示在当前考虑的物品中选取若干个,使得总重量不超过j的情况下,能够获得的最大价值int[] dp = new int[W+1];// 遍历每一个物品for(int i = 1; i<=n; i++) {// 从后往前更新dp[j]for(int j = W; j>=weight[i]; j--) {// 选取第i个物品,更新最大价值dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);}}// 返回在所有物品中选取若干个,使得总重量不超过W的情况下,能够获得的最大价值return dp[W];
}

这段代码的时间复杂度仍然是 O ( n ∗ W ) O(n*W) O(nW),但是空间复杂度降低到了 O ( W ) O(W) O(W),其中 n n n是物品的数量, W W W是背包的容量。

注意,我们后面会经常使用空间压缩的版本,因此需要吃透这份代码。

模板题 | 采药

我们来看一道洛谷上的模板题。

测试链接:P1048 [NOIP2005 普及组] 采药

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 2 2 个整数 T T T 1 ≤ T ≤ 1000 1 \le T \le 1000 1T1000)和 M M M 1 ≤ M ≤ 100 1 \le M \le 100 1M100),用一个空格隔开, T T T 代表总共能够用来采药的时间, M M M 代表山洞里的草药的数目。

接下来的 M M M 行每行包括两个在 1 1 1 100 100 100 之间(包括 1 1 1 100 100 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

样例

样例输入

70 3
71 100
69 1
1 2

样例输出

3

提示

【数据范围】

  • 对于 30 % 30\% 30% 的数据, M ≤ 10 M \le 10 M10
  • 对于全部的数据, M ≤ 100 M \le 100 M100

【题目来源】

NOIP 2005 普及组第三题

解题

这道题就是标准的01背包问题,每种草药只能采摘一次,也就是说每种物品只能选择一次或者不选择,不能选择多次。

我们定义dp[i][j]为在前i种草药中选取若干种,使得总时间不超过j的情况下,能够获得的最大价值。对于第i种草药,我们可以选择采摘,也可以选择不采摘。如果我们选择采摘,那么我们需要在剩余的时间j-weight[i]中选择前i-1种草药,使得总价值最大;如果我们选择不采摘,那么我们需要在时间j中选择前i-1种草药,使得总价值最大。

我们取这两种情况的最大值,就是dp[i][j]的值。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;public class Main {static int N = 101; // 草药的最大数量static int W = 1001;// 总时间的最大值static int[] weight = new int[N]; // 每种草药的采摘时间static int[] value = new int[N]; // 每种草药的采摘价值static int n, w; // 分别表示草药的数量和总时间public static void main(String[] args) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(System.in));StreamTokenizer in = new StreamTokenizer(br);PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));while (in.nextToken() != StreamTokenizer.TT_EOF) {w = (int) in.nval;in.nextToken();n = (int) in.nval;for (int i = 1; i <= n; i++) {in.nextToken();weight[i] = (int) in.nval;in.nextToken();value[i] = (int) in.nval;}out.println(compute2());}out.flush();out.close();br.close();}// 经典解法public static int compute1() {// dp[i][j]表示在前i个草药中选取若干个,使得总时间不超过j的情况下,能够获得的最大价值int[][] dp = new int[n + 1][w + 1];for (int i = 1; i <= n; i++) {for (int j = 0; j <= w; j++) {// 不要i号草药dp[i][j] = dp[i - 1][j];if (j - weight[i] >= 0) {// 要i号草药dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - weight[i]] + value[i]);}}}return dp[n][w];}// 空间压缩版本public static int compute2() { int[] dp = new int[w + 1];for (int i = 1; i <= n; i++) {for (int j = w; j >= weight[i]; j--) {dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);}}return dp[w];}}

力扣题单

链接题解
2915. 和为目标值的最长子序列的长度题解
416. 分割等和子集题解
494. 目标和题解
2787. 将一个数字表示成幂的和的方案数题解
1049. 最后一块石头的重量 II题解

这篇关于【算法 | 背包专题】01背包(解题思路+题单)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/883171

相关文章

C++入门01

1、.h和.cpp 源文件 (.cpp)源文件是C++程序的实际实现代码文件,其中包含了具体的函数和类的定义、实现以及其他相关的代码。主要特点如下:实现代码: 源文件中包含了函数、类的具体实现代码,用于实现程序的功能。编译单元: 源文件通常是一个编译单元,即单独编译的基本单位。每个源文件都会经过编译器的处理,生成对应的目标文件。包含头文件: 源文件可以通过#include指令引入头文件,以使

代码随想录算法训练营:12/60

非科班学习算法day12 | LeetCode150:逆波兰表达式 ,Leetcode239: 滑动窗口最大值  目录 介绍 一、基础概念补充: 1.c++字符串转为数字 1. std::stoi, std::stol, std::stoll, std::stoul, std::stoull(最常用) 2. std::stringstream 3. std::atoi, std

人工智能机器学习算法总结神经网络算法(前向及反向传播)

1.定义,意义和优缺点 定义: 神经网络算法是一种模仿人类大脑神经元之间连接方式的机器学习算法。通过多层神经元的组合和激活函数的非线性转换,神经网络能够学习数据的特征和模式,实现对复杂数据的建模和预测。(我们可以借助人类的神经元模型来更好的帮助我们理解该算法的本质,不过这里需要说明的是,虽然名字是神经网络,并且结构等等也是借鉴了神经网络,但其原型以及算法本质上还和生物层面的神经网络运行原理存在

大林 PID 算法

Dahlin PID算法是一种用于控制和调节系统的比例积分延迟算法。以下是一个简单的C语言实现示例: #include <stdio.h>// DALIN PID 结构体定义typedef struct {float SetPoint; // 设定点float Proportion; // 比例float Integral; // 积分float Derivative; // 微分flo

LeetCode 算法:二叉树的中序遍历 c++

原题链接🔗:二叉树的中序遍历 难度:简单⭐️ 题目 给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。 示例 1: 输入:root = [1,null,2,3] 输出:[1,3,2] 示例 2: 输入:root = [] 输出:[] 示例 3: 输入:root = [1] 输出:[1] 提示: 树中节点数目在范围 [0, 100] 内 -100 <= Node.

【Java算法】滑动窗口 下

​ ​    🔥个人主页: 中草药 🔥专栏:【算法工作坊】算法实战揭秘 🦌一.水果成篮 题目链接:904.水果成篮 ​ 算法原理 算法原理是使用“滑动窗口”(Sliding Window)策略,结合哈希表(Map)来高效地统计窗口内不同水果的种类数量。以下是详细分析: 初始化:创建一个空的哈希表 map 用来存储每种水果的数量,初始化左右指针 left

ScrollView 往上滑动,里面的一个View停在某个位置的思路

1.scrollView的contentoffset 为view的左上角,减去此时scrollView的左上角 2.而且还不需要让那个红色的view removeFromSuperView ,直接self.view AddSubView 就会自动从原来的那个View脱离开来 3.以后遇到问题的思路。当发现UIView很许多奇特的效果的时候,思考它是不是在不断的改变父控件。 #pragma m

ROS2从入门到精通4-4:局部控制插件开发案例(以PID算法为例)

目录 0 专栏介绍1 控制插件编写模板1.1 构造控制插件类1.2 注册并导出插件1.3 编译与使用插件 2 基于PID的路径跟踪原理3 控制插件开发案例(PID算法)常见问题 0 专栏介绍 本专栏旨在通过对ROS2的系统学习,掌握ROS2底层基本分布式原理,并具有机器人建模和应用ROS2进行实际项目的开发和调试的工程能力。 🚀详情:《ROS2从入门到精通》 1 控制插

算法与数据结构面试宝典——回溯算法详解(C#,C++)

文章目录 1. 回溯算法的定义及应用场景2. 回溯算法的基本思想3. 递推关系式与回溯算法的建立4. 状态转移方法5. 边界条件与结束条件6. 算法的具体实现过程7. 回溯算法在C#,C++中的实际应用案例C#示例C++示例 8. 总结回溯算法的主要特点与应用价值 回溯算法是一种通过尝试各种可能的组合来找到所有解的算法。这种算法通常用于解决组合问题,如排列、组合、棋盘游

【图像识别系统】昆虫识别Python+卷积神经网络算法+人工智能+深度学习+机器学习+TensorFlow+ResNet50

一、介绍 昆虫识别系统,使用Python作为主要开发语言。通过TensorFlow搭建ResNet50卷积神经网络算法(CNN)模型。通过对10种常见的昆虫图片数据集(‘蜜蜂’, ‘甲虫’, ‘蝴蝶’, ‘蝉’, ‘蜻蜓’, ‘蚱蜢’, ‘蛾’, ‘蝎子’, ‘蜗牛’, ‘蜘蛛’)进行训练,得到一个识别精度较高的H5格式模型文件,然后使用Django搭建Web网页端可视化操作界面,实现用户上传一