蓝桥杯每日一题------背包问题(一)

news2025/1/19 2:40:26

背包问题

阅读小提示:这篇文章稍微有点长,希望可以对背包问题进行系统详细的讲解,在看的过程中如果有任何疑问请在评论区里指出。因为篇幅过长也可以进行选择性阅读,读取自己想要的那一部分即可。

前言

背包问题可以看作动态规划系列入门的一个开端,欢迎开启动态规划之旅,在正式学习之前,我想说的是,动态规划真的不难,与贪心算法比较,动态规划有自己的多种板子,也有自己的多种套路;与高级数据结构比较,动态规划的代码量真的非常友好;与字符串类算法比较,动态规划没有那么抽象,ok话不多说,开始吧。
首先介绍一下动态规划的步骤(我自己总结的,自己用起来感觉还不错,y总也有介绍过闫式dp分析法,大家感兴趣可以看一看,怎么方便怎么来)
求解动态规划有两个大的阶段,分别是定义dp数组和推导状态转移方程。大家觉得这两个哪个重要呢?诚然状态转移方程是动态规划的关键,但是我在做题的过程中感受到当你的dp数组定义正确了,状态转移方程的推导就是自然而然的事情,所以对我来说,最关键的是定义dp数组。我们可以按照下面的步骤定义dp数组。
第一步:缩小规模。大家在大学学到动态规划时,一般都会拿来和贪心比,和分治比,无论哪一个我们都不能一口吃个胖子,都是从最基础的那个地方开始,一步一步往下走,最终走到终点。既然要缩小规模,那必然要有一个维度来定义当前的规模,放在背包问题里,规模就是考虑的物品的个数,那么用一个维度就可以了,放在区间dp里,规模是区间的大小,而不同的区间结果是不一样的,所以需要两个维度来表示区间的左右端点。
第二步:限制。放在背包问题里,限制就是背包的容量,你选的物品的总体积不能超过当前背包容量,所以你需要一个维度来表示当前的体积。
第三步:写出dp数组。走到这里,根据规模和限制定义了dp数组,dp[i][j]表示当前考虑了前i个物品,背包容量为j时能够装的最大价值。我们求的就是最大价值,那么dp数组对应的值就是最大价值,一般和所求是一样的,求什么就记录什么。
第四步:修改dp数组。这一步就是在写状态转移方程时,你发现定义的dp数组维度少了,还需要其它信息,那么这个时候就是需要什么往dp数组里面加什么,即增加维度,但是要注意一点,一般dp数组的维度和时间复杂度是正相关的,维度过多,很有可能超时。

01背包

在这里插入图片描述
定义dp数组
第一步:缩小规模。考虑n个物品,那我就先考虑1个物品,在考虑2个物品…,需要一个维度表示当前考虑的物品个数。
第二步:限制。所选物品个数不能超过物品容量,那么需要一个维度记录当前背包的容量。
第三步:写出dp数组。dp[i][j]表示当前考虑了前i个物品,背包容量为j时的最大价值。
第四步:推状态转移方程。dp[i][j]应该从哪里转移过来呢,必然是从前i-1个物品转移,我要考虑两种情况,对于第i个物品,可以选择要它,也可以不要它,如果要第i个物品,我就需要背包里面给我预留出第i个物品的体积,也就是从a[i-1][j-v[i]]转移,同时也能获得该物品的价值。如果不要第i个物品,那么之前从前一个状态相同容量的背包转移过来就行,即a[i-1][j]。
综上状态转移方程如下
a[i][j] = max(a[i-1][j],a[i-1][j-v[i]]+w[i])
考虑写代码了
第一步:确定好遍历顺序,对于背包问题,一般第一个for遍历规模,第二个for遍历限制。

for(int i = 1;i <= n;i++) {
		for(int j = 1;j <= m;j++) {
			dp[i][j] = dp[i-1][j];//为什么要在这里转移,因为这个转移是一定会发生的,而另一个转移不一定会发生
			if(j>=v[i])
				dp[i][j] = Math.max(dp[i-1][j-v[i]]+w[i], dp[i][j]);
		}
	}

第二步:考虑是否要对dp数组初始化,这里不需要,因为最开始的状态考虑前0个物体,它的值就是0,不需要管。
全部代码如下,

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int V = scanner.nextInt();
	int[] v = new int[n+1];
	int[] w = new int[n+1];
	for (int i = 1; i < w.length; i++) {
		v[i] = scanner.nextInt();
		w[i] = scanner.nextInt();
	}
	int[][] dp = new int[n+1][V+1];
//	for (int i = 0; i < dp.length; i++) {
//		dp[0][i] = 1;
//	}
	for (int i = 1; i < dp.length; i++) {
		for (int j = 0; j < V+1; j++) {
			dp[i][j] = Math.max(dp[i][j], dp[i-1][j]);
			if(v[i]<=j) {
				dp[i][j] = Math.max(dp[i][j], dp[i-1][j-v[i]]+w[i]);
			}
		}
	}
	System.out.println(dp[n][V]);
}
}

考虑对dp数组进行维度优化,这里的优化并不会降低它的时间复杂度,但是可以减低空间复杂度,提高空间利用率,并且它也可以算是滚动dp的一个因子,而且里面有一个思想在后续做题的过程中也需会用到!
我们考虑一下在转移的过程中我只用了a[i]和a[i-1]对于a[i-2],a[i-3]我后续都用不到了,所以没有必要存它,考虑如果我只用一个一维的dp,思路还是一样的,但是代码该怎么写。
令dp[i]表示背包容量为i时最多能容纳的物品价值。自己尝试把代码里表示物品个数的那一维删掉,就成了

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int V = scanner.nextInt();
	int[] v = new int[n+1];
	int[] w = new int[n+1];
	for (int i = 1; i < w.length; i++) {
		v[i] = scanner.nextInt();
		w[i] = scanner.nextInt();
	}
	int[] dp = new int[V+1];
	for (int i = 1; i < dp.length; i++) {
		for (int j = 0; j < V+1; j++) {
			//dp[j] = Math.max(dp[j], dp[j]);
			if(v[i]<=j) {
				dp[j] = Math.max(dp[j], dp[j-v[i]]+w[i]);
			}
		}
	}
	System.out.println(dp[V]);
}
}

直接这样提交可以过吗?当然不可以,我们还记得我们的题目是每个物品只有一个吗?我们分析一下dp[j] = Math.max(dp[j], dp[j-v[i]]+w[i]);
假设当前遍历到了i=5,假设j=5时,dp[j]=dp[j-v[i]]+w[i].说明此时我们拿了第5个物品,当遍历到j=10时假设此时v[i]=5,dp[10]=dp[10-5]+w[i]=dp[5]+w[i],可以看见dp[10]是从dp[5]转移的,但是我们的本意是不是dp[5]表示的应该是i=4时的结果,但是刚刚我们也看见了,遍历到dp[10]时,dp[5]已经被更新了,它不是i=4时的dp[5],所以会出错。好,我们再深究一下,出错的结果是啥?dp[5]是不是已经选了物品5了?此时dp[10]==dp[5]+w[i]又选了一次物品5,说明物品5被选了多次,而题目要求每个物品只能选一次,所以不符合题意。如果改一改,改成每个物品可以选无数次,那么这里就是没有问题,记住这一点。
回到这个题目,那我们应该怎么改,在求dp[10]时,会用到dp[5],归纳一下,在求dp[i]时,会用到dpj,我们在遍历到i之前不能动dp[j]。也就是说,先遍历大的数,所以我们直接倒序遍历就行了。来看代码吧,

	for (int j = 0; j < n; j++) {
			for (int i = k; i >= v[j]; i--) {//i<v[j]时不能转移,所以直接遍历到v[j]就行,这样后面就不用if语句判断是否能转移了。
				dp[i] = Math.max(dp[i], dp[i - v[j]] + w[j]);
			}
		}

全部代码

import java.io.IOException;
import java.util.Scanner;
public class Main {
	public static void main(String[] args) throws IOException {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int k = scanner.nextInt();
		int[] v = new int[n];
		int[] w = new int[n];
		for (int i = 0; i < n; i++) {
			v[i] = scanner.nextInt();
			w[i] = scanner.nextInt();
		}
		int[] dp = new int[k + 1];
		for (int j = 0; j < n; j++) {
			for (int i = k; i >= v[j]; i--) {
				// System.out.println("---");
				dp[i] = Math.max(dp[i], dp[i - v[j]] + w[j]);
			}
		}
		System.out.println(dp[k]);
	}
}

借此机会,再讲一下滚动dp,他不算是单独的一种dp,只是对dp的一种空间优化方法,防止爆内存。刚刚讲过,在dp数组遍历的过程中我只用到了当前为i时的状态和前一个为i-1时的状态,其它的都不要了,所以其实我可以把dp[n+1][V+1],变成dp[2][V+1],如果dp[0][V+1]表示考虑了前0个物品的状态,遍历到i=1时,用dp[1][V+1]表示考虑了前1个物品的状态,遍历到i=2时,前0个物品的状态我不需要记录了,此时可以拿dp[0][V+1]表示考虑了前2个物品的状态,如此循环往复。可以发现这是交替使用的,那么数字里面什么是交替出现的?奇偶数呀,所以可以用奇偶数来判断,如dp[i&1][j]和dp[(i-1)&1][j]。在使用滚动dp时,其实修改很好修改,只要在你原来的代码里,注意是使用二维数组的那个代码哈,把dp[i][j]和dp[i-1][j]改成dp[i&1][j]和dp[(i-1)&1][j]就行了。因此它也不易出错,比起刚刚介绍的直接把dp数组减少一维。看代码吧。

import java.io.IOException;
import java.util.Scanner;
public class Main {
	public static void main(String[] args) throws IOException {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int k = scanner.nextInt();
		int[] v = new int[n + 1];
		int[] w = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			v[i] = scanner.nextInt();
			w[i] = scanner.nextInt();
		}
		int[][] dp = new int[2][k + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 0; j <= k; j++) {
				// System.out.println(i + " " + j + " ---------");
				if (j >= v[i]) {
					dp[i&1][j] = Math.max(dp[(i - 1)&1][j], dp[(i - 1)&1][j - v[i]] + w[i]);
				} else {
					// System.out.println(i + " " + j);
					dp[i&1][j] = dp[(i - 1)&1][j];
				}

			}
		}
		System.out.println(dp[n&1][k]);
	}
}

完全背包

在这里插入图片描述
完全背包和01背包的不同在于完全背包对每个物品的可选次数没有限制,那么在遍历的时候就会比原来多出一个维度,dp数组的定义还是一样的,dp[i][j]表示考虑前i个物品当前背包容量为j时的最大价值。那么可选物品不受限制如何体现呢?
01背包在递推dp数组时有两个嵌套for循环,第一层遍历当前考虑前i个物品,第二层遍历当前背包的容量为j,那么我们需要加入一个维度,这个维度表示选择j2个第i个物品,完整代码如下

import java.util.Scanner;
public class Main {
	public static void main(String[] args) {
      		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int k = scanner.nextInt();
		int[] v = new int[n + 1];
		int[] w = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			v[i] = scanner.nextInt();
			w[i] = scanner.nextInt();
		}
		int[][] dp = new int[n + 1][k + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j < k + 1; j++) {
				for (int j2 = 0; j2 * v[i] <= j; j2++) {
					dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - j2 * v[i]] + j2 * w[i]);
				}

			}
		}
		System.out.println(dp[n][k]);
	}
}

此时的复杂度就是 O ( n 3 ) O(n^3) On3。我们来回顾一下,我们之前有没有类似的代码。在将01背包压缩成1维时,我们是不是有一种错误写法,第二维如果正序遍历会导致同一个物品被多次选择,这对于01背包来说是不合题意的,但是正好符合完全背包的要求,所以之前那个错误的代码完全可以用到完全背包上,并且这个的时间复杂度只需要 O ( n 2 ) O(n^2) On2,代码如下。

import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int k = scanner.nextInt();
		int[] v = new int[n + 1];
		int[] w = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			v[i] = scanner.nextInt();
			w[i] = scanner.nextInt();
		}
		int[] dp = new int[k + 1];
		for (int i = 1; i <= n; i++) {
//			for (int j = 0; j < dp.length && j >= v[i]; j++) {
			for (int j = v[i]; j < dp.length; j++) {
//				System.out.println(dp[i] + " " + (dp[j - v[i]] + w[i]) + " " + i + " " + j);
				dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
			}
		}
//		for (int i = 0; i < dp.length; i++) {
//			System.out.print(dp[i] + " ");
//		}
		System.out.println(dp[k]);
	}
}

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

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

相关文章

js手写Promise(上)

目录 构造函数resolve与reject状态改变状态改变后就无法再次改变 代码优化回调函数中抛出错误 thenonFulfilled和onRejected的调用时机异步then多个then 如果是不知道或者对Promise不熟悉的铁铁可以先看我这篇文章 Promise 构造函数 在最开始&#xff0c;我们先不去考虑Promi…

精简还是全能?如何在 Full 和 Lite 之间做出最佳选择!关于Configuration注解的Full模式与Lite模式(SpringBoot2)

&#x1f3c3;‍♂️ 微信公众号: 朕在debugger© 版权: 本文由【朕在debugger】原创、需要转载请联系博主&#x1f4d5; 如果文章对您有所帮助&#xff0c;欢迎关注、点赞、转发和订阅专栏&#xff01; 前言 关于 Configuration 注解&#xff0c;相信在座的各位 Javaer 都…

可达鸭二月月赛——基础赛第六场(周五)题解,这次四个题的题解都在这一篇文章内,满满干货,含有位运算的详细用法介绍。

姓名 王胤皓 T1 题解 T1 题面 T1 思路 样例输入就是骗人的&#xff0c;其实直接输出就可以了&#xff0c;输出 Hello 2024&#xff0c;注意&#xff0c;中间有一个空格&#xff01; T1 代码 #include<bits/stdc.h> using namespace std; #define ll long long int …

Swift 使用 Combine 管道和线程进行开发 从入门到精通八

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

ANSI Escape Sequence 下落的方块

ANSI Escape Sequence 下落的方块 1. ANSI Escape 的用途 无意中发现 B站有人讲解&#xff0c; 完全基于终端实现俄罗斯方块。 基本想法是借助于 ANSI Escape Sequence 实现方方块的绘制、 下落动态效果等。对于只了解 ansi escape sequence 用于 log 的颜色打印的人来说&…

(每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第10章 项目进度管理(四)

博主2023年11月通过了信息系统项目管理的考试&#xff0c;考试过程中发现考试的内容全部是教材中的内容&#xff0c;非常符合我学习的思路&#xff0c;因此博主想通过该平台把自己学习过程中的经验和教材博主认为重要的知识点分享给大家&#xff0c;希望更多的人能够通过考试&a…

【Java EE】----SpringBoot的日志文件

1.SpringBoot使用日志 先得到日志对象通过日志对象提供的方法进行打印 2.打印日志的信息 3.日志级别 作用&#xff1a; 可以筛选出重要的信息不同环境实现不同日志级别的需求 ⽇志的级别分为&#xff1a;&#xff08;1-6级别从低到高&#xff09; trace&#xff1a;微量&#…

高级数据结构与算法 | 布谷鸟过滤器(Cuckoo Filter):原理、实现、LSM Tree 优化

文章目录 Cuckoo Filter基本介绍布隆过滤器局限变体 布谷鸟哈希布谷鸟过滤器 实现数据结构优化项Victim Cache备用位置计算半排序桶 插入查找删除 应用场景&#xff1a;LSM 优化 Cuckoo Filter 基本介绍 如果对布隆过滤器不太了解&#xff0c;可以看看往期博客&#xff1a;海量…

CentOS 7安装Nodejs

说明&#xff1a;本文介绍如何在云服务器上CentOS 7操作系统上安装Nodejs。以及安装过程中遇到的问题。 下载压缩包&解压 首先&#xff0c;先去官网下载Linux版本的Node。 将下载下来的压缩包&#xff0c;上传到云服务器上&#xff0c;解压。配置环境变量。 &#xff08…

VScode为什么选择了Electron,而不是QT?

选择Electron而不是QT可能是基于以下几个原因&#xff1a; Web技术的普及和开发者生态系统&#xff1a;Web技术如HTML、CSS和JavaScript在开发者中非常普及&#xff0c;开发者生态系统庞大且活跃。使用Electron可以利用这些熟悉的Web技术和丰富的开发者社区资源。跨平台支持&am…

蓝桥杯(Web大学组)2022国赛真题:水果消消乐

思路&#xff1a; 记录点击次数&#xff0c;点击次数为1时&#xff0c;记录点击下标&#xff08;用于隐藏or消除&#xff09;、点击种类&#xff0c;点击次数为2时&#xff0c;判断该下标所对应种类与第一次是否相同 相同&#xff1a;两个都visibility:hidden &#xff08;占…

黄金交易策略(EA):三个仓位的设计是确保可以不停息做单

完整EA&#xff1a;Nerve Knife.ex4黄金交易策略_黄金趋势ea-CSDN博客

MacOS 查AirPods 电量技巧:可实现低电量提醒、自动弹窗

要怎么透过macOS 来查询AirPods 电量呢&#xff1f;当AirPods 和Mac 配对后&#xff0c;有的朋友想通过Mac来查询AirPods有多少电量&#xff0c;这个里有几个技巧&#xff0c;下面我们来介绍一下。 透过Mac 查AirPods 电量技巧 技巧1. 利用状态列上音量功能查询 如要使用此功能…

在django中集成markdown文本框

首先需要下载开源组件&#xff1a;http://editor.md.ipandao.com/&#xff0c;可能需要挂梯子。 百度网盘&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1D9o3P8EQDqSqfhAw10kYkw 提取码&#xff1a;eric 1.在html代码中生成一个div&#xff0c;ideditor <div c…

【玩转408数据结构】线性表——定义和基本操作

考点剖析 线性表是算法题命题的重点&#xff0c;该类题目实现相对容易且代码量不高&#xff0c;但需要最优的性能&#xff08;也就是其时间复杂度以及空间复杂度最优&#xff09;&#xff0c;这样才可以获得满分。所以在考研复习中&#xff0c;我们需要掌握线性表的基本操作&am…

Peter算法小课堂—枚举优化

哈哈哈&#xff0c;新年快乐&#xff01;这一次Peter将要给大家讲一讲轻松、摆烂的算法—枚举&#xff01;咋就是说呀&#xff0c;枚举这个玩意我语法就会了。但大家想想&#xff0c;咱们CSP考试时&#xff08;除了没过初赛的&#xff09;只给1秒&#xff0c;大家想想&#xff…

跟着cherno手搓游戏引擎【23】项目维护、2D引擎之前的一些准备

项目维护&#xff1a; 修改文件结构&#xff1a; 头文件自己改改就好了 创建2DRendererLayer&#xff1a; Sandbox2D.h: #pragma once #include "YOTO.h" class Sandbox2D :public YOTO::Layer {public:Sandbox2D();virtual ~Sandbox2D() default;virtual void O…

【lesson46】进程通信之system V(共享内存)

文章目录 共享内存通信原理用共享内存通信shmServer.ccshmClient.cc 完整通信代码common.hppLog.hppshmServer.ccshmClient.cc通信测试 共享内存借助管道添加访问控制common.hppshmServer.ccshmClient.cc 共享内存通信原理 两个进程将一块system V的物理地址通过页表映射到自己…

JSP编程

JSP编程 您需要理解在JSP API的类和接口中定义的用于创建JSP应用程序的各种方法的用法。此外,还要了解各种JSP组件,如在前一部分中学习的JSP动作、JSP指令及JSP脚本。JSP API中定义的类提供了可借助隐式对象通过JSP页面访问的方法。 1. JSP API的类 JSP API是一个可用于创建…

大模型学习 一

https://www.bilibili.com/video/BV1Kz4y1x7AK/?spm_id_from333.337.search-card.all.click GPU 计算单元多 并行计算能力强 指数更重要 A100 80G V100 A100 海外 100元/时 单卡 多卡并行&#xff1a; 单机多卡 模型并行 有资源的浪费 反向传播 反向传播&#xff08;B…