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种选择。
- 放弃DP做法 改用DFS
- 坚持DP,但是把值放进状态,变成判定性问题,前提你的时间和空间不会爆炸
- 给博主来个一键三连
众所周知,这里是背包问题,背包问题是满足最优子结构的,否则他不会成为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......
今日块引用严重超标
!?人家是偷梁换柱,你是偷梁换梁,根本没变。
不仅如此,人家是偷梁换柱,你是偷int
换bool
,反倒空间更优了!
原方程: 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
数组,这很普通。
第二次求,你会选择:
- 正序枚举 j j j
- 逆序枚举 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!