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<n≤30, 1 ≤ V ≤ 20000 1 \le V \le 20000 1≤V≤20000。

【题目来源】

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代码。

这里引入一个新算法:递归

你应该尝试过

cpp 复制代码
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 n≤30,有可能T,但是赌一赌,说不定数据太弱了~

递归Code

cpp 复制代码
#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; //不要输出ans
	return 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 j≡k( 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 j≡k,但如果删掉 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(i−1,j)f(i−1,j−ai)+ai j≤ai}

OK现在可以写代码了

Code

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

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码
                   _ooOoo_
                  o8888888o
                  88" . "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 j≡k,状态可以删除 j j j并加入 k k k

j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 j j j并加入 k k k
j ≡ k j\equiv k j≡k,状态可以删除 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(i−1,j)f(i−1,j−ai)+ai j≤ai}

但是 + 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(i−1,j)f(i−1,j−ai) j≤ai}

Code

直接在源码上修改

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码
                   _ooOoo_
                  o8888888o
                  88" . "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 i−1行,这会导致行数混乱!

所以要逆序枚举 j j j,以前的决策都是 i − 1 i-1 i−1行的,自然而然正确完成DP。

在原来普通DP的基础上,直接删除第一维度,逆序枚举就能AC,并且空间不会爆炸。

废话不多说,看代码吧!

Code

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
/*
下面是防伪代码
                   _ooOoo_
                  o8888888o
                  88" . "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!

相关推荐
前端小刘哥4 小时前
新版视频直播点播平台EasyDSS,打通远程教研与教师培训新通路
算法
kobe_t4 小时前
数据安全系列7:常用的非对称算法浅析
算法
靠近彗星4 小时前
3.4特殊矩阵的压缩存储
数据结构·人工智能·算法
清辞8535 小时前
C++入门(底层知识C与C++的不同)
开发语言·c++·算法
fqbqrr5 小时前
2510C++,api设计原则,不除零
开发语言·c++
~kiss~5 小时前
图像处理~多尺度边缘检测算法
图像处理·算法·计算机视觉
fqbqrr5 小时前
2510d,C++虚混杂
c++·d
Mr.看海5 小时前
机器学习鼻祖级算法——使用SVM实现多分类及Python实现
算法·机器学习·支持向量机
科比不来it5 小时前
Go语言数据竞争Data Race 问题怎么检测?怎么解决?
开发语言·c++·golang