动态规划——背包问题入门
1. 01背包
概述
给出N个物品,每个物体都具有一定的体积和价值,而每个物体都只有一个。 拥有一个V体积的背包,问应如何装包才能使背包中物体的总价值最大?
解题思路
背包问题是典型的动态规划类题目,而动态规划是典型的通过规律找出正解的方法。所以解题思路的关键在于如何寻找不同数据之间的关系(状态转移)。
直接描述不方便解释,我们以例题为例:
(注:01背包问题不涉及因为物体的形状、大小等而如何放入背包的问题,只是单纯考虑体积和价值。我们可以理解成每个物体都是液体,而这个背包是个大水缸(当然,每种液体要么全倒进去,要么一点不倒,不允许倒一部分。))
例:HDU2602 Bone Collector
Problem
Many years ago , in Teddy’s hometown there was a man who was called “Bone Collector”. This man like to collect varies of bones , such as dog’s , cow’s , also he went to the grave …
The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?
简单释义
很久以前,在Teddy的家乡有一个被称作 “骨头收集者” 的人。这个人喜欢收集各式各样的骨头 —— 狗的,牛的,当然他也经常会去墓地转转......(是个狼灭)
这个人有一个总容量为V的背包,而在他的旅途中他有很多骨头要收集。而很明显,不同的骨头有着不同的体积和不同的价值。现在给定他找到的每个骨头都是什么样的,请计算这人能装进包里、收集到的骨头的最大价值是多少?
Input
The first line contain a integer T , the number of cases.
Followed by T cases , each case three lines , the first line contain two integer N , V, (N <= 1000 , V <= 1000 )representing the number of bones and the volume of his bag. And the second line contain N integers representing the value of each bone. The third line contain N integers representing the volume of each bone.
输入格式
输入第一行包含一个整数T,代表不同情况的数量(多组数据)。
跟随的T种情况一个情况有三行,第一行包含两个整数N、V(N<=1000,V<=1000)代表骨头的数量和他背包的容量。第二行包含N个整数代表每块骨头的价值。第三行包含N个整数代表每块骨头的体积。
Output
One integer per line representing the maximum of the total value (this number will be less than \(2^{31}\)).
输出格式
每行一个整数,代表总价值的最大值(这个数字不大于\(2^{31}\))。
输入样例
1
5 10
1 2 3 4 5
5 4 3 2 1
输出样例
14
题目分析
动态规划,即要求在各种可能的局部的解中,找到那些可能达到最优的解的局部解并找到最优解,而放弃其他的解,以此实现“大事化小,小事化了”。
以本题来说,这个狼灭装包有成千上万种方法,就有成千上万种局部解,而我们要找到的是价值最大的那一种,而这一种是成千上万种方法中的一种,也就是局部解的一种,而这一种是最优解。
我们给一组数据:
1
3 5
5 12 20
1 2 4
首先我们可以这样保存数据:
这个狼灭的每个骨头的体积是v[b],价值是w[b],给每个骨头编个号就是b,在前面已经声明好了:
int T;
scanf("%d",&T);
for(int c=1;c<=T;c++)
{
int N,V;
scanf("%d%d",&N,&V);
for(int b=1;b<=N;b++)
scanf("%d",&w[b]);
for(int b=1;b<=N;b++)
scanf("%d",&v[b]);
}
而他选择要如何装包的过程,就是他 做决策 的过程。
先来看他拿的第一块骨头:
这块骨头的价值是5,体积是1。
我们只有这一块骨头,那么无论我们的背包有多大,我们都只能放这一块骨头进去。那么就有这样的关系:
总价值 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...... | 背包体积 |
---|---|---|---|---|---|---|---|---|---|
1 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | ...... | |
骨头编号 |
现在这个狼灭又捡来了他的第二块骨头:那么他的背包体积要是比3大的话,他就可以装进两块骨头了:
——
他的背包体积为1,他只能放一块骨头1,价值是5,这也是最大价值;
他的背包体积为2,他可以放一块骨头2,价值是12,是最大价值;
他的背包体积为3,他可以放一块骨头2和一块骨头1,价值是17,是最大价值;
他的背包体积为4,他仍然只可以放一块骨头2和一块骨头1,价值是17,价值最大;
接下来他的背包体积再怎么大,他也只能放一块骨头2和一块骨头1,所以往后价值都是17:
总价值 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...... | 背包体积 |
---|---|---|---|---|---|---|---|---|---|
1 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | ...... | |
2 | 5 | 12 | 17 | 17 | 17 | 17 | 17 | ...... | |
骨头编号 |
接下来他搞来了他的第三块骨头:体积4,价值20。
——
他的背包体积为1,他只能放一块骨头1,价值是5,这也是最大价值;
他的背包体积为2,他可以放一块骨头2,价值是12,是最大价值;
他的背包体积为3,他可以放一块骨头2和一块骨头1,价值是17,是最大价值,到这和前面都一样。
他的背包体积为4,他就可以放一块骨头3了,那价值是20,最大了;
他的背包体积为5,他可以放一块骨头3外加一块骨头1,这是25;
他的背包体积为6,他可以放一块骨头3外加一块骨头2,这是32;
他的背包体积为7,他可以放一块骨头3、一块骨头2、一块骨头1,那么就是37;
再往后的话,他无论如何也只能装这3块骨头了,往后都是37。
总价值 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...... | 背包体积 |
---|---|---|---|---|---|---|---|---|---|
1 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | ...... | |
2 | 5 | 12 | 17 | 17 | 17 | 17 | 17 | ...... | |
3 | 5 | 12 | 17 | 20 | 25 | 32 | 37 | ...... | |
骨头编号 |
……
既然有了这么个表,我们便可以把这个表记下来。我们来一个数组:
int dp[i][j];//在这里,表示考虑到前i块骨头,背包体积为j,可以获得的最大价值。那么dp[i][j]就是这种情况下的最大价值。
那么这个数组就是:
dp[i][j] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...... | j |
---|---|---|---|---|---|---|---|---|---|
1 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | ...... | |
2 | 5 | 12 | 17 | 17 | 17 | 17 | 17 | ...... | |
3 | 5 | 12 | 17 | 20 | 25 | 32 | 37 | ...... | |
i |
乍一看好像没什么规律,而接下来就是关键——在这个表中找出数据的规律,这也就是动态规划的精华:推导状态转移方程 ——也就是这个狼灭做决策的过程。
然而这个狼灭拿到了一块骨头i,他需要考虑的问题只不过是他要不要把这块骨头i放进包。
他要是决定不放,那么他决策完了就是:dp[i][j]=dp[i-1][j];
——包里东西没变,和[i-1][j]时候的包是一样的;
他要是决定放,那么他决策完了就是:dp[i][j]=dp[i-1][j-v[i]]+w[i];
——他往包里放了一个i号骨头,那么这时候包里就有了这块i号骨头,而体积也被占去了v[i],但价值多了w[i]——于是反过来想,他放进去之后的dp[i][j]——--》就是他没有放i、体积是j-v[i]时候的那个状态加上i的价值《--!!
(简单的东西拿字母表示出来就有些烧脑了。。。QAQ)
于是他要是决定不放,那么状态[i][j]就是从[i-1][j]那里转移过来的;
他要是决定放,那么状态[i][j]就是从[i-1][j-v[i]]那里转移过来的;
他放不放都行,而我们要找的是价值最大的,所以这个时候的状态就是——
dp[i][j]=max(dp[i-1][j-v[i]]+w[i],dp[i-1][j]);//Note:j>=v[i];
这就是这道题的状态转移方程,接下来我们只消得把它塞进程序里就OK了:
#include<cstdio>
#include<algorithm> //max()的头
using namespace std;
int dp[1080][1080];
int w[1080],v[1080];
int main()
{
int T;
scanf("%d",&T);
for(int c=1;c<=T;c++)
{
int N,V;
scanf("%d%d",&N,&V);
for(int b=1;b<=N;b++)
scanf("%d",&w[b]);
for(int b=1;b<=N;b++)
scanf("%d",&v[b]);
for(int i=1;i<=N;i++)//i个物品
for(int j=0;j<=V;j++)//背包体积为j——转两个循环列举各个情况的状态,并用不同的状态进行转移:
{
if(j>=v[i])//使用状态转移方程
dp[i][j]=max(dp[i-1][j-v[i]]+w[i],dp[i-1][j]);
if(j<v[i])//如果此时背包的体积比这个物体的体积小,那无论如何也放不进去,所以走“不放”的状态转移
dp[i][j]=dp[i-1][j];
}
printf("%d\n",dp[N][V]);
}
return 0;
}
……
可以看出来了:他要决定怎么放,那么他的决策有千万多种多,在这里面找出最优状态当然会累S。
而我们从他只有1块骨头开始,推导出两块骨头、三块骨头……并找出其中的联系,就可以找到他有N块骨头应该怎么放,那么我们便很容易在不断的转移过程中找出最优解,这也就是动态规划的“大事化小,小事化了”思维的精华所在。