动态规划——背包问题入门

1. 01背包

概述

给出N个物品,每个物体都具有一定的体积和价值,而每个物体都只有一个。 拥有一个V体积的背包,问应如何装包才能使背包中物体的总价值最大?

解题思路

背包问题是典型的动态规划类题目,而动态规划是典型的通过规律找出正解的方法。所以解题思路的关键在于如何寻找不同数据之间的关系(状态转移)。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

直接描述不方便解释,我们以例题为例:

(注: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块骨头应该怎么放,那么我们便很容易在不断的转移过程中找出最优解,这也就是动态规划的“大事化小,小事化了”思维的精华所在。

未完待续。。。 算了还是完了吧。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄