当前位置: 首页 > news >正文

P1049 装箱问题 题解(四种方法)附DP和DFS的对比

P1049 [NOIP 2001 普及组] 装箱问题

题目描述

有一个箱子容量为 V V V,同时有 n n n 个物品,每个物品有一个体积。

现在从 n n n 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。

输入格式

第一行共一个整数 V V V,表示箱子容量。

第二行共一个整数 n n n,表示物品总数。

接下来 n n n 行,每行有一个正整数,表示第 i i i 个物品的体积。

输出格式

  • 共一行一个整数,表示箱子最小剩余空间。

输入输出样例 #1

输入 #1

24
6
8
3
12
7
9
7

输出 #1

0

说明/提示

对于 100 % 100\% 100% 数据,满足 0 < n ≤ 30 0<n \le 30 0<n30 1 ≤ V ≤ 20000 1 \le V \le 20000 1V20000

【题目来源】

NOIP 2001 普及组第四题

题目大意

给定由 n n n个正整数组成的序列 a a a和一个正整数 V V V
a a a中选取若干个数,
要求这些数字的和不得超过 V V V
V − V- V最大的和的值。

递归求解

如果你是一个暴躁的人
在这里插入图片描述
你绝对会想到暴力
但是你不能写一个 30 30 30重循环,构成 13 13 13代码。
在这里插入图片描述
这里引入一个新算法:递归
你应该尝试过

int main()
{return main();
}

吧?
这段代码函数无限调用自身,会导致栈溢出(Stack Overflow),这就叫递归:函数内部调用自身。
但是,如果你往函数里加入一个参数,并当参数超限时自动return;,就可以避免这种情况!
我们定义递归函数void f(i,j)表示正在处理第i个物品,其最大占用空间为j
在每个函数里都有两个决策:要和不要

不要

这个很简单,只需要调用f(i+1,j)就行了。

如果背包容量够,即j+a[i]<=V,才能调用。
if(j+a[i]<=V)f(i+1,j+a[i]);

出口

递归总得有一个出口吧,不然就无限递归引发栈溢出了。
i>n时,所有物品都处理完了,这时候与ans比较,更新ans的值。
不用担心i推不到n+1但是更优,忘了我们有不要选择了吧?
这样一个完美程序就设计好了。
先试试能不能AC, n ≤ 30 n\le30 n30,有可能T,但是赌一赌,说不定数据太弱了~

递归Code

#include<bits/stdc++.h>
using namespace std;
int n,V,a[31],ans;
void f(int i,int j)
{if(i>n){ans=max(ans,j);return;}f(i+1,j); //不要if(j+a[i]<=V)f(i+1,j+a[i]); //要
}
int main()
{cin>>V>>n;for(int i=1;i<=n;i++)cin>>a[i];f(1,0);cout<<V-ans; //不要输出ansreturn 0; //好习惯
}

在这里插入图片描述
可爱的样例,为我们代码争气了
来看看提交的结果吧~
在这里插入图片描述
看看不开O2会怎样
在这里插入图片描述
细节编程语言失去了O2
只能说数据太蒻了吧~
竞赛的时候不建议哦~

动态规划

这是一道典型的背包DP,以物品划分阶段没错了。
然后判定是否合法的时候,要用到空间占用度,并且对于相同的状态,后面的问题相等,因此这是二维DP,一维物品,一维空间,数组存储最大值,~~最后输出f[n][m]~~空间有可能没用满,因此要输出*max_element(f[n],f[n]+m+1),即f[n]这一行的最大值。

状态

f(i,j)=k表示前i个物品处理完成后,所占空间为j的最大占用空间为k
发现了吧, j ≡ k j\equiv k jk j j j恒等于 k k k),但不要着急,不要删掉 j j j
这里引入新概念:
在这里插入图片描述
叫做最优子结构。
什么意思?意思是当前状态只需要最优的子问题推来即可满足最优子结构。
当前有一个问题,它有许多子问题,dp选择最优的子问题的结果,其他直接kill掉,反正有人比你更优。
但是有些时候,f(i-1)的当前状态对于f(i)的贡献是最优的,但是当前状态不一定对f(i+1)是最优的。
举个栗子,刷过短剧吧,偏心类的不止一次见了吧?
我交白卷,是为了防止你抄我的答案
上一世,同学们都偏向他,drive a car to bangbangbang me,and say to me,我能在1m之内听到你的心声
这一世,我不会让这场悲剧重演
往往都是最有能力的人最被忽视,不满足最优子结构的题目也是一样。
例如我们带入《质数孤独/我交白卷你慌什么》,f(i-1).a表示第一个决策,f(i-1).b表示第二个决策
人物分配:
f(i-1).a:江源(抄袭大王)
f(i-1).b:江北(奥数天才)
f(i):江家的长辈(不爱江北 溺爱江源)
f(i+1):保姆、同学、评委(保持清醒 坚信江北 反对江源)
(不用看完整版,看个免费部分就行了)
f(i-1).a对于f(i)来说是最优的,就像江家的长辈溺爱江源
f(i-1).b对于f(i)来说不是最优的,就像江家的长辈不爱江北
f(i-1).b推到f(i)后对于f(i+1)是最优的,就像其他人时刻保持清醒 认为江北是最好的
f(i-1).a推到f(i)后对于f(i+1)不是最优的,就像其他人都知道江源是抄袭大王
实际上其他人的思想是正确的,因为越靠后的状态越可能被加入输出范围。
那为什么会出现下图描述的情况呢?
在这里插入图片描述
也就是说,江北和江源的命运是交叉的。
之所以出现这种情况,是因为这道dp题目不满足最优子结构,而不满足最优子结构的题目,你有3种选择。

  1. 放弃DP做法 改用DFS
  2. 坚持DP,但是把值放进状态,变成判定性问题,前提你的时间和空间不会爆炸
  3. 给博主来个一键三连

众所周知,这里是背包问题,背包问题是满足最优子结构的,否则他不会成为DP模板。
回归正题~
f(i,j)=k表示前i个物品处理完成后,其占用度为j,此时最大占用度为k
666双重占用度,其实 j ≡ k j\equiv k jk,但如果删掉 j j j,变成f(i)=j会带来什么后果?
没错,他会不满足最优子结构,为何?
在这里插入图片描述
答案在此~

当前这个原问题要由子问题推出来,我一定要知道子问题占用多少空间,才能看看现在的是否合法呀。
我现在想要求原问题,看看子问题。
可恶啊,就给我一个f(i-1),里面就这一个值,其他都被顶掉了啊啊啊啊啊!
但其他的实际上是合法的,我白白丢失了这么多状态啊!我这包WA的啊啊啊啊啊!
这……让我想想,是什么在作祟?
对啦,正是最优子结构
这道题一维会GG,不满足最优子结构,再也不敢乱删除状态变量辣!😭

所以,千万不要乱删状态变量哦~

方程

搬运背包模板方程,价值和体积皆为a
f ( i , j ) = max ⁡ { f ( i − 1 , j ) f ( i − 1 , j − a i ) + a i j ≤ a i } f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)+a_i\ j\le a_i\end{Bmatrix} f(i,j)=max{f(i1,j)f(i1,jai)+ai jai}
OK现在可以写代码了

Code

为了防止抄袭,我特意加入了防伪代码
感谢这位博主上传了佛祖保佑代码(链接能点)

#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码_ooOoo_o8888888o88" . "88(| -_- |)O\  =  /O____/`---'\____.'  \\|     |//  `./  \\|||  :  |||//  \/  _||||| -:- |||||-  \|   | \\\  -  /// |   || \_|  ''\---/''  |   |\  .-\__  `-`  ___/-. /___`. .'  /--.--\  `. . __."" '<  `.___\_<|>_/___.'  >'"".| | :  `- \`.;`\ _ /`;.`/ - ` : | |\  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======`=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^佛祖保佑       永无BUG
复制来源:https://blog.csdn.net/vbirdbest/article/details/78995793
*/
int a[31],V,n,f[31][20009];
int main()
{cin>>V>>n;for(int i=1;i<=n;i++)cin>>a[i];//$f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)+a_i\ j\le a_i\end{Bmatrix}$for(int i=1;i<=n;i++){for(int j=0;j<=V;j++) //不要写int j=1 因为题目说可以不选{f[i][j]=f[i-1][j];if(j>=a[i])f[i][j]=max(f[i][j],f[i-1][j-a[i]]+a[i]);}}//最后输出V-*max_element(f[n],f[n]+V+1)而非V-f[n][V]cout<<V-*max_element(f[n],f[n]+V+1);return 0; //坏习惯养成
}

样例ac
在这里插入图片描述
提交ac
在这里插入图片描述
这种做法相对简单,但是 j k j\ k j k重复,状态可以删除 j j j并加入 k k k,仍然保持原来的最优子结构。
千万不要输出V-f[n][V],即使AC,也是数据太弱,真正考试的时候可能根本装不满!

布尔DP

这种做法相对简单,但是 j k j\ k j k重复,状态可以删除 j j j并加入 k k k,仍然保持原来的最优子结构。

“做人没梦想就是条咸鱼,小鸡没梦想变炸鸡。”——《小鸡吃米》作者:优秀少年好好
必须去实现!
在这里插入图片描述
请重复这句话: j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k

j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k jk,状态可以删除 j j j并加入 k k k
……

今日块引用严重超标
!?人家是偷梁换柱,你是偷梁换梁,根本没变。
不仅如此,人家是偷梁换柱,你是偷intbool,反倒空间更优了!
原方程: f ( i , j ) = max ⁡ { f ( i − 1 , j ) f ( i − 1 , j − a i ) + a i j ≤ a i } f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)+a_i\ j\le a_i\end{Bmatrix} f(i,j)=max{f(i1,j)f(i1,jai)+ai jai}
但是 + a i +a_i +ai不适用于bool类型,于是你思考起含义——这就是看当前状态可不可达!
在这里插入图片描述
关键时刻我的脑子好用了~
取个逻辑或就行了。
但是C++没有||=运算符,你也没法重载,不过~
bool类型的 max ⁡ \max max就是逻辑或,因此你可以取 max ⁡ \max max
注意赋初值f(0,0)=true

方程

f ( i , j ) = max ⁡ { f ( i − 1 , j ) f ( i − 1 , j − a i ) j ≤ a i } f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)\ j\le a_i\end{Bmatrix} f(i,j)=max{f(i1,j)f(i1,jai) jai}

Code

直接在源码上修改

#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码_ooOoo_o8888888o88" . "88(| -_- |)O\  =  /O____/`---'\____.'  \\|     |//  `./  \\|||  :  |||//  \/  _||||| -:- |||||-  \|   | \\\  -  /// |   || \_|  ''\---/''  |   |\  .-\__  `-`  ___/-. /___`. .'  /--.--\  `. . __."" '<  `.___\_<|>_/___.'  >'"".| | :  `- \`.;`\ _ /`;.`/ - ` : | |\  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======`=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^佛祖保佑       永无BUG
复制来源:https://blog.csdn.net/vbirdbest/article/details/78995793
*/
int a[31],V,n;bool f[31][20009];
int main()
{cin>>V>>n;for(int i=1;i<=n;i++)cin>>a[i];f[0][0]=1;//$f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)+a_i\ j\le a_i\end{Bmatrix}$for(int i=1;i<=n;i++){for(int j=0;j<=V;j++) //不要写int j=1 因为题目说可以不选{f[i][j]=f[i-1][j];if(j>=a[i])f[i][j]=max(f[i][j],f[i-1][j-a[i]]); //max=||}}//扫描f[n]的第最后一个true值int ans=0;for(int i=0;i<=V;i++)if(f[n][i])ans=i;cout<<V-ans;return 0; //坏习惯养成
}

样小例AC
在这里插入图片描述
提大交AC
在这里插入图片描述
一个防伪代码能把我的代码干到1.23KB
今日删除线严重超标
这种方法就是为了满足最优子结构而引出来的一个应对措施,如果想让f为一维数组可以用滚动数组

滚动数组(一维)

新概念

在这里插入图片描述
W H A T I S 滚动数组 ? WHAT\ IS\ 滚动数组? WHAT IS 滚动数组?
滚动数组,顾名思义,数组滚动着使用,可以减少存储i-2行及以前的退休状态,这些退休状态不在我们当前行的有关子问题状态范围内,他们存着太浪费了,滚动数组就能将这些空间循环再利用,节省内存,这种方法必须掌握!
在这里插入图片描述
今日图片数量严重超标
今日解题方案数严重超标
今日删除线严重超标
今日严重超标严重超标
今日严重超标严重超标
今日严重超标严重超标
今日严重超标严重超标
……
系统错误:递归栈溢出
首先第一次求dp数组,这很普通。
第二次求,你会选择:

  1. 正序枚举 j j j
  2. 逆序枚举 j j j

如果你正序枚举 j j j,那么恭喜你噶了,why?
正序枚举的话 j j j以前的决策都是第 i i i行的,而非第 i − 1 i-1 i1行,这会导致行数混乱!
所以要逆序枚举 j j j,以前的决策都是 i − 1 i-1 i1行的,自然而然正确完成DP。
在原来普通DP的基础上,直接删除第一维度,逆序枚举就能AC,并且空间不会爆炸。
废话不多说,看代码吧!

Code

#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码_ooOoo_o8888888o88" . "88(| -_- |)O\  =  /O____/`---'\____.'  \\|     |//  `./  \\|||  :  |||//  \/  _||||| -:- |||||-  \|   | \\\  -  /// |   || \_|  ''\---/''  |   |\  .-\__  `-`  ___/-. /___`. .'  /--.--\  `. . __."" '<  `.___\_<|>_/___.'  >'"".| | :  `- \`.;`\ _ /`;.`/ - ` : | |\  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======`=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^佛祖保佑       永无BUG
复制来源:https://blog.csdn.net/vbirdbest/article/details/78995793
*/
int a[31],V,n,f[20009];
int main()
{cin>>V>>n;for(int i=1;i<=n;i++)cin>>a[i];//$f(i,j)=\max\begin{Bmatrix}f(i-1,j)\\f(i-1,j-a_i)+a_i\ j\le a_i\end{Bmatrix}$for(int i=1;i<=n;i++){for(int j=V;j>=a[i];j--) //这里可以直接枚举到a[i],省时省力,为啥我刚才忘加了啊{//f[j]=f[j]不对吧?f[j]=max(f[j],f[j-a[i]]+a[i]); //取max是必不可少的}}//最后不能输出V-f[V] ac也是数据弱爆了cout<<V-*max_element(f+,f+V+1);return 0; //坏习惯养成
}

今日佛像严重超标
我曾欸西过样例,也曾欸西过整题
在这里插入图片描述
在这里插入图片描述

总结

这道题可以用DP/DFS去做,看似简单的一道背包DP却藏着多种解法与多种思想,不愧是NOIP!

附录:DP和DFS的异同

相同之处

他们都来源于同一颗搜索树,阶段、决策全都相同

不同之处

DP

DP用多重循环分阶段穷举状态,所以相同状态只会穷举一次,合并重复的状态。
但是盲目穷举所有可能的状态会造成冗余(读rǒng yú)

DFS

DFS在当前状态的函数里,自己调用自己,从而产生新的状态,不会有任何冗余,但不是记忆化搜索,所以会重复,造成TLE,内存只需要当前状态占用空间以及递归栈的调用就够了。

DP&DFS

DP:当前状态是通过上一个相邻的状态出来的,必须最优子结构(对于不满足的,把值放进状态,但是要求不能TLE/MLE)
DFS:当前函数调用下一个相邻状态,暴力枚举所有可能性,不会有被忽略的“江北”,不需要最优子结构
O ( 2 n ) O(2^n) O(2n)问题中, n = 100 n=100 n=100,DP第 100 100 100层是 100 100 100个状态,DFS第 100 100 100层是 2 100 2^{100} 2100个状态!
高精度算一下, 2 100 = 1267650600228229401496703205376 2^{100}=1267650600228229401496703205376 2100=1267650600228229401496703205376
在这里插入图片描述
这是一个天文数字!TLE不用说了~

冗余和重复

冗余:决策不连续,做了很多无用功,空的这些叫冗余
重复:决策相同时,计算了很多次,使得时间超慢!
具体做题的时候哪个更害人,要根据题目来看。
God Baye!

http://www.dtcms.com/a/503620.html

相关文章:

  • Windows下Vscode连接到WSL的方法
  • R语言系列入门教程:什么是R语言?与传统编程语言有什么区别?
  • 商务网站建设的主流程网页设计排版作品分析
  • Altium Designer(AD24)原理图菜单栏详细介绍
  • 【JavaWeb学习】关于mysql-connector-j版本过高引起的问题
  • Eudemon1000E-F_V600R024C00SPC100
  • 建设工程资质录入是在那个网站机械类网站模板
  • 手机网站建站用哪个软件好字体样式 网站
  • ESMO中国之声丨徐兵河教授:芦康沙妥珠单抗再奏ADC中国之声,HR阳性HER2阴性晚期乳腺癌迎来CDK4/6抑制剂治疗后新希望
  • 模板网站禁止右键wordpress描述代码
  • pyhton(大厂笔试/面试)最长子序列(哈希-回溯-中等)含源码(二十三)
  • 做淘宝浏览单的网站菏泽外贸网站建设公司
  • Linux:理解操作系统和进程
  • 单片机开发工具篇:(六)STM32CubeMX 的使用,包括软件和固件包的下载、以及基础使用
  • 网站建设费是多少常州高端网站建设
  • 20.UE-游戏逆向-绘制所有对象坐标
  • jsp网站建设作业泗阳县建设局网站
  • Springboot整合IoTB
  • 个人做网站哪种类型的网站好男生做男生网站在那看
  • 从 0 到 1 学 C 语言队列:链表底层实现(初始化 / 入队 / 出队 / 销毁),代码可直接复用!
  • 书店网站建设网站栏目结构软文营销的特点有哪些
  • 做个网站要多久做网站app要多少钱
  • 1. Linux 驱动开发前景
  • 深入理解进程生命周期:从 fork 到 exit 的完整旅程
  • 英维克(002837)-2025-10-19
  • 自助手机网站建站软件wordpress metaslider
  • PCIe协议之 Equalization篇 之 FIR 三抽头的三因子的理解
  • FFmpeg 基本API av_seek_frame函数内部调用流程分析
  • FFmpeg 基本API avcodec_send_packet函数内部调用流程分析
  • 手机建站网站常德营销型网站建设