蓝桥杯——算法训练——开心的金明(附0-1背包讲解)
0
0
0-
1
1
1 背包的简单应用。
——————————————————————————————————————————————————
资源限制
时间限制:1.0s 内存限制:256.0MB
问题描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过
N
N
N元钱就行”。
今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的
N
N
N元。于是,他把每件物品规定了一 个重要度,分为
5
5
5等:用整数
1
∼
5
1\sim5
1∼5表示,第
5
5
5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。
他希望在不超过
N
N
N元(可以等于
N
N
N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第
j
j
j 件物品的价格为
v
[
j
]
v[j]
v[j],重要度为
p
[
j
]
p[j]
p[j],共选中了
k
k
k 件物品,编号依次为
j
1
,
j
2
,
…
,
j
k
j_1,j_2,\dots,j_k
j1,j2,…,jk,则所求的总和为:
v
[
j
1
]
×
p
[
j
1
]
+
v
[
j
2
]
×
p
[
j
2
]
+
⋯
+
v
[
j
k
]
×
p
[
j
k
]
v[j_1]\times p[j_1]+v[j_2]\times p[j_2]+\dots+v[j_k]\times p[j_k]
v[j1]×p[j1]+v[j2]×p[j2]+⋯+v[jk]×p[jk]
请你帮助金明设计一个满足要求的购物单。
输入格式
输入文件的第
1
1
1 行,为两个正整数,用一个空格隔开:
N
m
N\space m
N m,其中
N
(
<
30000
)
N(<30000)
N(<30000)表示总钱数,
m
(
<
25
)
m(<25)
m(<25) 为希望购买物品的个数。
从第
2
2
2 行到第
m
+
1
m+1
m+1 行,第
j
j
j 行给出了编号为
j
−
1
j-1
j−1 的物品的基本数据,每行有
2
2
2 个非负整数:
v
p
v\space p
v p,其中
v
v
v 表示该物品的价格
(
v
⩽
10000
)
(v\leqslant10000)
(v⩽10000),
p
p
p 表示该物品的重要度
(
1
∼
5
)
(1\sim5)
(1∼5)
输出格式
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值
(
<
100000000
)
(<100000000)
(<100000000)。
样例输入
1000 5
800 2
400 5
300 5
400 3
200 2
样例输出
3900
——————————————————————————————————————————————————
思路分析:刚拿到这道题的时候,如果没有了解过背包问题,很可能误以为是贪心,但如果我们仔细想一想,这道题的关键实际上应该是最大化地利用给定的金额
N
N
N ,因此它应该是一道背包题。背包问题种类繁多,这里涉及到的是最简单的
0
0
0-
1
1
1背包,下面我们借助该题来看一下
0
0
0-
1
1
1 背包的核心思路:
现在我们有
m
m
m 件希望购买的物品,它们的价格与重要度分别为
v
i
,
p
i
v_i,p_i
vi,pi,题目要求给出不超过总钱数的物品的价格与重要度乘积的总和的最大值,这看起来有点烦,我们简化一下,定义
w
i
=
v
i
×
p
i
w_i=v_i\times p_i
wi=vi×pi 为每件物品的价值,那么我们要做的就是从这些物品中挑选出总价格不超过
N
N
N 的物品,然后求出所有挑选方案中价值总和的最大值。
对于这
m
m
m 个物品,每种物品只有两个选择:买或者不买,那么这
m
m
m 个物品就对应了
2
m
2^m
2m 个挑选方案。最朴素的思路——枚举,我们将这
2
m
2^m
2m 个方案枚举出来,分别价值总和,最后返回最大的。相信大家都听说过指数爆炸,显然,这个方案的时间复杂度是不可接受的。优化算法
i
n
g
!
ing!
ing!
先定义量
R
e
c
(
m
,
n
)
Rec(m,\space n)
Rec(m, n):剩余金额为
n
n
n,可购买物品为
1
∼
m
1\sim m
1∼m 时,所能取得的最大化背包价值(即已购买物品的价值之和)。这里我们来仔细分析一下金明同学购买物品时的心路历程:
现在还有第
1
∼
i
1\sim i
1∼i 个物品可以买啦,但是手上的钱只有
j
j
j。这时根据是否购买第
i
i
i 个物品,我们可以给出两个方案。
a. 不买:那么很简单,第
i
i
i 个物品直接被
p
a
s
s
pass
pass 掉,将目光投向第
1
∼
(
i
−
1
)
1\sim( i-1)
1∼(i−1) 个,剩余金额自然没有变化,那么得到
R
e
c
(
i
,
j
)
=
R
e
c
(
i
−
1
,
j
)
Rec(i,\space j)=Rec(i-1,\space j)
Rec(i, j)=Rec(i−1, j);
b. 买:首先我们的剩余金额变为
j
−
v
[
i
]
j-v[i]
j−v[i],接下来要做的就是如何用着
j
−
v
[
i
]
j-v[i]
j−v[i] 继续购买第
1
∼
(
i
−
1
)
1\sim(i-1)
1∼(i−1) 件物品,那么
R
e
c
(
i
,
j
)
=
R
e
c
(
i
−
1
,
j
−
v
[
i
]
)
+
w
[
i
]
Rec(i,\space j)=Rec(i-1,j-v[i])+w[i]
Rec(i, j)=Rec(i−1,j−v[i])+w[i]
我们现在要干什么?当然是从这两个方案中选出价值更高的,依据这一原则,得到如下递推式:
R
e
c
(
i
,
j
)
=
max
(
R
e
c
(
i
−
1
,
j
)
,
R
e
c
(
i
−
1
,
j
−
v
[
i
]
)
+
w
[
i
]
)
Rec(i,\space j)=\max\big(Rec(i-1,j),\space Rec(i-1,j-v[i])+w[i]\big)
Rec(i, j)=max(Rec(i−1,j), Rec(i−1,j−v[i])+w[i])
但是,这一递推式并非最终结果,因为我们还有一些细节没有考虑到:
a. 如果压根就没有物品可供购买,那么我们拿再多的钱都没有意义,即
R
e
c
(
0
,
j
)
=
0
Rec(0,\space j)=0
Rec(0, j)=0;
b. 如果手上的钱少于第
i
i
i 件物品,那么我们也压根儿用不着考虑是否购买它,直接得到
R
e
c
(
i
,
j
)
=
R
e
c
(
i
−
1
,
j
)
Rec(i,\space j)=Rec(i-1,\space j)
Rec(i, j)=Rec(i−1, j)
综上,我们得到
0
0
0-
1
1
1 背包的通用递推式:
R e c ( i , j ) = { 0 i = = 0 R e c ( i − 1 , j ) j < v [ i ] max ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − v [ i ] ) + w [ i ] ) Rec(i,\space j)=\begin{cases}0\hspace{1cm}i==0\\\\Rec(i-1,\space j)\hspace{1cm}j<v[i]\\\\\max\big(Rec(i-1,j),\space Rec(i-1,j-v[i])+w[i]\big)\end{cases} Rec(i, j)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧0i==0Rec(i−1, j)j<v[i]max(Rec(i−1,j), Rec(i−1,j−v[i])+w[i])
下面我们试着利用递推式解决金明的问题,首先自然想到递归法,但是请你们画张图走一下流程,这个算法的时间和空间复杂度都是非常高的,指数级的,没意义。我们再来仔细想一想递推式的推导过程,我们采取的策略实际上是把大问题分解为子问题,最后合并子问题的最优解,得到大问题的最优解,相信有的同学已经看出来了——这就是动态规划呀!OK,下面直接把递推式转化为动态规划的核心——动态方程:
d p [ i ] [ j ] = { 0 i = = 0 d p [ i − 1 ] [ j ] j < v [ i ] max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + v [ i ] ) dp[i][j]=\begin{cases}0\hspace{1cm}i==0\\\\dp[i-1][j]\hspace{1cm}j<v[i]\\\\\max\big(dp[i-1][j],\space dp[i-1][j-v[i]]+v[i]\big)\end{cases} dp[i][j]=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧0i==0dp[i−1][j]j<v[i]max(dp[i−1][j], dp[i−1][j−v[i]]+v[i])
解决这道问题,代码如下:
#include <stdio.h>
#include <string.h>
#include <iomanip>
#include <algorithm>
using namespace std;
int dp[30][30005], w[30], v[30];
//dp[i][j]存储的值为还剩j元时,还有1~i可以选取,w[i]存储i的重要程度,v[i]存储i的价格
int main(){
memset(dp, 0, sizeof(dp));
memset(w, 0, sizeof(w));
memset(v, 0, sizeof(v));//初始化
int n, m;//共有n元,m件物品
scanf("%d%d", &n, &m);
for(int i=1; i<=m; i++) scanf("%d%d", &v[i], &w[i]);
for(int i=1; i<=m; i++) w[i]*=v[i];//根据题意改写重要度,为重要等级与物品价格的乘积
for(int i=1; i<=m; i++){//注意是从1开始
for(int j=0; j<=n; j++){
if(j<v[i]) dp[i][j]=dp[i-1][j];//如果剩余的钱少于第i件物品,直接不予考虑
else dp[i][j]=max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
//若剩余的钱还可以买第i件物品,比较重要度,选取更高的
}
}
printf("%d\n", dp[m][n]);
return 0;
}
实际上我们用动态规划解决
0
0
0-
1
1
1 背包问题的时候是可以优化一下空间使用的,即讲
d
p
dp
dp 数组由二维降成一维,从而降低算法的空间复杂度。如果我们了解过动态规划,就会知道
d
p
dp
dp 数组的作用是将子问题的最优解进行打表,从而在合并子问题求解大问题最优解的时候直接搜索。
那么我们就可以从打表的流程入手,优化一下空间使用。根据上面的代码,我们知道我们是一行一行,从左到右进行打表的,那么我们在填写第
i
i
i 行的值时,只依赖于上一行,那么我们直接开两行数组,再不断更新就可以了。但是,关键来了,动态方程的核心
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 实际上只取决于上一行的前
j
+
1
j+1
j+1 个值,那么我们用一行数组,就能完成动态规划:
用 dp[N] 存储不同金额能够购买的最大价值
(如果觉得有点难理解,建议手动模拟打表过程)
现在我们来看优化之后的代码:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int dp[30005], w[30], v[30];
int main(){
int n, m;
while(scanf("%d%d", &n, &m) != EOF){
memset(dp, 0, sizeof(dp));
memset(w, 0, sizeof(w));
memset(v, 0, sizeof(v));
for(int i=1; i<=m; i++){
scanf("%d%d", &v[i], &w[i]);
w[i]*=v[i];
}
for(int i=1; i<=m; i++){
for(int j=n; j>=0; j--){
if(j<v[i]) dp[j]=dp[j];
else dp[j]=max(dp[j], dp[j-v[i]]+w[i]);
}
}
printf("%d\n", dp[n]);
}
return 0;
}
效果显著
最后,给出 0 0 0- 1 1 1 背包模版:
const int MAX = 100000;//视情况而定
int dp[MAX];//初始化的值同样需要视情况而定
int vi[MAX], wi[MAX]; // v[i]:物品价值,w[i]:物品重量
//01背包
void ZeroOnePack(int n, int w){
//n:总共有n种物品; w:背包总共可以承受的重量
for(int i=1; i<=n; i++){
for(int j=w; j>=0; j--){
if(j<wi[i]) dp[j] = dp[j];
else dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
}
}
}