动态规划初步之背包问题(参考背包九讲+例题+详细分析+补充)

1 01背包问题
1.1 题目
有N件物品和一个容量为V 的背包。放入第i件物品耗费的空间是Ci,得到 的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
1.2 基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不 放。 用子问题定义状态:即F[i,v]表示前i件物品恰放入一个容量为v的背包可以 获得的最大价值。则其状态转移方程便是:

F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi} 

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生 出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化 为一个只和前i−1件物品相关的问题。如果不放第i件物品,那么问题就转化 为“前i−1件物品放入容量为v的背包中”,价值为F[i−1,v];如果放第i件物 品,那么问题就转化为“前i−1件物品放入剩下的容量为v −Ci的背包中”, 此时能获得的最大价值就是F[i−1,v −Ci]再加上通过放入第i件物品获得的价 值Wi。 伪代码如下:
 

F[0,0..V ] = 0 
for i = 1 to N 
    for v = Ci to V 
        F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi}

例:在使用动态规划算法求解0-1背包问题时,使用二维数组m[i][j]存储背包剩余容量为j,可选物品为i、i+1、……、n时0-1背包问题的最优值。绘制

价值数组v = {8, 10, 6, 3, 7, 2},

重量数组w = {4, 6, 2, 2, 5, 1},

背包容量C = 12时对应的m[i][j]数组。

0 1 2 3 4 5 6 7 8 9 10 11 12
1 0 0 0 8 8 8 8 8 8 8 8 8
2 0 0 0 8 8 10 10 10 10 18 18 18
3 0 6 6 8 8 14 14 16 16 18 18 24
4 0 6 6 9 9 14 14 17 17 19 19 24
5 0 6 6 9 9 14 14 17 17 19 21 24
6 2 6 8 9 11 14 16 17 19 19 21 24

第一行和第一列为序号,其数值为0)
如m[2][6],在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。

#include <iostream>
#include <cstring>
using namespace std;
 
 
const int N=15;
 
 
int main()
{
    int v[N]={0,8,10,6,3,7,2};
    int w[N]={0,4,6,2,2,5,1};
 
 
    int m[N][N];
    int n=6,c=12;
    memset(m,0,sizeof(m));
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=c;j++)
        {
            if(j>=w[i])
                m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
 
 
            else
                m[i][j]=m[i-1][j];
        }
    }
 
 
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=c;j++)
        {
            cout<<m[i][j]<<' ';
        }
        cout<<endl;
    }
 
 
    return 0;
}

1.3 优化空间复杂度
以上方法的时间和空间复杂度均为O(V N),其中时间复杂度应该已经不能 再优化了,但空间复杂度却可以优化到O(V )。 先考虑上面讲的基本思路如何实现,肯定是有一个主循环i = 1..N,每次 算出来二维数组F[i,0..V ]的所有值。那么,如果只用一个数组F[0..V ],能不 能保证第i次循环结束后F[v]中表示的就是我们定义的状态F[i,v]呢?F[i,v]是 由F[i−1,v]和F[i−1,v−Ci]两个子问题递推而来,能否保证在推F[i,v]时(也 即在第i次主循环中推F[v]时)能够取用F[i−1,v]和F[i−1,v −Ci]的值呢?事 实上,这要求在每次主循环中我们以v = V..0的递减顺序计算F[v],这样才能保 证推F[v]时F[v−Ci]保存的是状态F[i−1,v−Ci]的值。伪代码如下:

F[0..V ] = 0 
for i = 1 to N 
    for v = V to Ci 
        F[v] = max{F[v],F[v−Ci] + Wi}

其中的F[v] = max{F[v],F[v −Ci] + Wi}一句,恰就对应于我们原来的转移方 程,因为现在的F[v−Ci]就相当于原来的F[i−1,v−Ci]。如果将v的循环顺序 从上面的逆序改成顺序的话,那么则成了F[i,v]由F[i,v−Ci]推导得到,与本题 意不符。 事实上,使用一维数组解01背包的程序在后面会被多次用到,所以这里抽象 出一个处理一件01背包中的物品过程,以后的代码中直接调用不加说明。

 有了这个过程以后,01背包问题的伪代码就可以这样写:

for i = 1 to N 
    for v = V to C 
        F[v] = max(F[v],f[v−C] + W)


1.4 初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。 有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背 包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其 它F[1..V ]均设为−∞,这样就可以保证最终得到的F[V ]是一种恰好装满背包的 最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该 将F[0..V ]全部设为0。 这是为什么呢?可以这样理解:初始化的F数组事实上就是在没有任何物 品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量 为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的 背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非 必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的 价值为0,所以初始时状态的值也就全部为0了。 这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状 态转移之前的初始化进行讲解。

练习题hdu2602

#include<stdio.h>
#include<string.h>
int max(int a,int b)
{
    return a>b?a:b;
}
int main()
{
    int T,N,V;
    int i,j;
    int bag[1010],v[1010],w[1010];
    scanf("%d",&T);
    while(T--)
    {
        memset(bag,0,sizeof(bag));//把背包各个状态是的价值设为0
        scanf("%d%d",&N,&V);
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        for(i=0;i<N;i++)
            scanf("%d",w+i);
        for(i=0;i<N;i++) //第i个物品
            for(j=V;j>=w[i];j--){ //j状态的背包(计算重量为j是的最大价值)
                bag[j]=max(bag[j],bag[j-w[i]]+v[i]);
            }
        printf("%d\n",bag[V]);
    }
    return 0;
}

hdu2546

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int main()
{
    int N,V,m;
    int i,j;
    int bag[1010],v[1010];
    while(~scanf("%d",&N),N)
    {
        memset(bag,0,sizeof(bag));
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        sort(v,v+N);//通过排序把最贵的菜放到最后一个位置
        
        scanf("%d",&m);//把卡余额m看做背包容量
        //饭菜的价格即代表他的价值,也代表他的重量
        V=m-5;//我们的目的是把卡余额尽量刷到5元
        if(m<5){
            printf("%d\n",m);
            continue;
        }
        for(i=0;i<N-1;i++) //第i个菜
            for(j=V;j>=v[i];j--){
                bag[j]=max(bag[j],bag[j-v[i]]+v[i]);
            }
        printf("%d\n",m-v[N-1]-bag[V]);//减掉最贵的菜,和规划好的最大消费
    }
    return 0;
}

到这一步,可以确定的是可能获得的最大价值,但是我们并不清楚具体选择哪几样物品能获得最大价值。

另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。

m[n][c]为最优值,如果m[n][c]=m[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][c]继续构造最优解;当x[n]=1时,则由x[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。

void traceback()
{
    for(int i=n;i>1;i--)
    {
        if(m[i][c]==m[i-1][c])
            x[i]=0;
        else
        {
            x[i]=1;
            c-=w[i];
        }
    }
    x[1]=(m[1][c]>0)?1:0;
}

例:

某工厂预计明年有A、B、C、D四个新建项目,每个项目的投资额Wk及其投资后的收益Vk如下表所示,投资总额为30万元,如何选择项目才能使总收益最大?

Project

Wk

Vk

A

15

12

B

10

8

C

12

9

D

8

5

结合前面两段代码

#include <iostream>
#include <cstring>
using namespace std;
 
const int N=150;
 
int v[N]={0,12,8,9,5};
int w[N]={0,15,10,12,8};
int x[N];
int m[N][N];
int c=30;
int n=4;
void traceback()
{
    for(int i=n;i>1;i--)
    {
        if(m[i][c]==m[i-1][c])
            x[i]=0;
        else
        {
            x[i]=1;
            c-=w[i];
        }
    }
    x[1]=(m[1][c]>0)?1:0;
}
 
int main()
{
 
 
    memset(m,0,sizeof(m));
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=c;j++)
        {
            if(j>=w[i])
                m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
 
            else
                m[i][j]=m[i-1][j];
        }
    }/*
    for(int i=1;i<=6;i++)
    {
        for(int j=1;j<=c;j++)
        {
            cout<<m[i][j]<<' ';
        }
        cout<<endl;
    }
*/
    traceback();
    for(int i=1;i<=n;i++)
        cout<<x[i];
    return 0;
}

输出x[i]数组:0111,输出m[4][30]:22。

得出结论:选择BCD三个项目总收益最大,为22万元。

不过这种算法只能得到一种最优解,并不能得出所有的最优解。

2 完全背包问题
2.1 题目
有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种 物品的耗费的空间是Ci,得到的价值是Wi。求解:将哪些物品装入背包,可使 这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
2.2 基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从 每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、 取1件、取2件……直至取⌊V /Ci⌋件等很多种。

如果仍然按照解01背包时的思路,令F[i,v]表示前i种物品恰放入一个容量 为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方 程,像这样:
F[i,v] = max{F[i−1,v−kCi] + kWi |0 ≤ kCi ≤ v} 这跟01背包问题一样有O(V N)个状态需要求解,但求解每个状态的时 间已经不是常数了,求解状态F[i,v]的时间是O( v Ci ),总的复杂度可以认为 是O(NV Σ V Ci ),是比较大的。 将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01 背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是要 试图改进这个复杂度。

2.3 一个简单有效的优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满 足Ci ≤ Cj且Wi ≥ Wj,则将可以将物品j直接去掉,不用考虑。 这个优化的正确性是显然的:任何情况下都可将价值小耗费高的j换成物美 价廉的i,得到的方案至少不会更差。对于随机生成的数据,这个方法往往会大 大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度, 因为有可能特别设计的数据可以一件物品也去不掉。 这个优化可以简单的O(N2)地实现,一般都可以承受。另外,针对背包 问题而言,比较不错的一种方法是:首先将费用大于V 的物品去掉,然后使 用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可 以O(V + N)地完成这个优化。这个不太重要的过程就不给出伪代码了

2.4 转化为01背包问题求解
01背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为01背 包问题来解。 最简单的想法是,考虑到第i种物品最多选⌊V /Ci⌋件,于是可以把第i种物品转化为 ⌊V /Ci⌋件费用及价值均不变的物品,然后求解这个01背包问题。这样的做法 完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为01背包 问题的思路:将一种物品拆成多件只能选0件或1件的01背包中的物品。 更高效的转化方法是:把第i种物品拆成费用为Ci2k、价值为Wi2k的若干件 物品,其中k取遍满足Ci*2^k ≤ V 的非负整数。 这是二进制的思想。因为,不管最优策略选几件第i种物品,其件数写成 二进制后,总可以表示成若干个2k件物品的和。这样一来就把每种物品拆 成O(log V Ci )件物品,是一个很大的改进。

2.5 O(V N)的算法
这个算法使用一维数组,先看伪代码:


F[0..V ] = 0 
for i = 1 to N 
    for v = Ci to V 
        F[v] = max(F[v],F[v−Ci] + Wi)

你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。 为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来 循环。让v递减是为了保证第i次循环中的状态F[i,v]是由状态F[i−1,v −Ci]递 推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入 第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F[i− 1,v −Ci]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加 选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结 果F[i,v−Ci],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的 程序为何成立的道理。 值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有 可能会带来算法时间常数上的优化。 这个算法也可以由另外的思路得出。例如,将基本思路中求解F[i,v−Ci]的 状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成 这种形式:

F[i,v] = max(F[i−1,v],F[i,v−Ci] + Wi) 

将这个方程用一维数组实现,便得到了上面的伪代码。
2.6 小结
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程。但是归根结底都要转化成01背包,要牢记不同点。

注意:

和01背包仅有一点区别,就是循环顺序。但是动态规划出来的结果是截然不同的!

             先回忆一下01背包为什么要到这循环,因为每一个物品在装进去的时候,都要用到前一个状态的数据来计算当前物品要不要加入。简单说,就是由前面物品装好的状态推出当前物品装入的状态。

             而内循环为正序时,先更新状态,再用更新了的状态继续更新当前状态,就产生了一个现象:在不超背包容量的状态下,我可以一直用当前物品不断更新背包状态,即不断往背包里填同一个物品。打个比方,我想在正在装一个重m的物品,进入内循环,我先把f [m] 这个状态更新了,想在f [m]就是装入了m后的最大价值,继续更新f [m+1]的状态,我会用到 f [j]=max( f [j], f [j-v[i]]+v[i]); 这个语句,也就是会调用到更小重量时的状态,而我恰好在这之前更新过了,那不就是我可以第二次把物品m装进背包吗。以此类推,我往后更新状态过程中,我可以无限的把同一个物品 i 往里装。

例题3:HDU1114

题意:某人有个存钱罐(存硬币),空着的时候重E,存满钱是重F。现在他把罐子装满钱了,给出硬币种类和重量,计算存钱罐最少能存多少钱。

思路:完全背包。每种硬币可以无限放。但是这里不是求最大价值,而是求最小价值,所以我们先假设所有状态为正无穷,然后不断更新dp,以求得最小价值。这样在动态规划时需要注意一点,状态0始终未0,即f [ 0 ] =0恒成立

#include<stdio.h>
#include<algorithm>
using namespace std;
const int INF=0x6fffffff;
int f[10010];
int main()
{
    int T,E,F,M,N;
    int p,w; //价值和重量
    scanf("%d",&T);
    while(T--)
    {
        for(int i=1;i<=10002;i++)f[i]=INF;
        f[0]=0;//这一步很重要,给动态规划一个开头
        scanf("%d%d%d",&E,&F,&N);
        M=F-E; //最大钱数M
        for(int i=0;i<N;i++){ //第 i 种硬币
            scanf("%d%d",&p,&w);
            for(int j=w;j<=M;j++) //不断往里放这种硬币,取钱数少的方案
                f[j]=min(f[j],f[j-w]+p);
        }
        if(f[M]==INF)
            puts("This is impossible.");
        else
            printf("The minimum amount of money in the piggy-bank is %d.\n",f[M]);
    }
    return 0;
}

3 多重背包问题
3.1 题目
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
3.2 基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 因为对于第i种物品有Mi+1种策略:取0件,取1件……取Mi件。令F[i,v]表 示前i种物品恰放入一个容量为v的背包的最大价值,则有状态转移方程: F[i,v] = max{F[i−1,v−k∗Ci] + k∗Wi |0 ≤ k ≤ Mi} 复杂度是O(V ΣMi)。

3.3 转化为01背包问题
另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成Mi件01 背包中的物品,则得到了物品数为ΣMi的01背包问题。直接求解之,复杂度仍 然是O(V ΣMi)。 但是我们期望将它转化为01背包问题之后,能够像完全背包一样降低复杂 度。 仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问 题中第i种物品可取的每种策略——取0...Mi件——均能等价于取若干件代换 以后的物品。另外,取超过Mi件的策略必不能出现。 方法是:将第i种物品分成若干件01背包中的物品,其中每件物品有一个系 数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数 分别为1,2,22 ...2k−1,Mi −2k + 1,且k是满足Mi −2k + 1 > 0的最大整数。例 如,如果Mi为13,则相应的k = 3,这种最多取13件的物品应被分成系数分别 为1,2,4,6的四件物品。 分成的这几件物品的系数和为Mi,表明不可能取多于Mi件的第i种物品。另 外这种方法也能保证对于0...Mi间的每一个整数,均可以用若干个系数的和表 示。这里算法正确性的证明可以分0...2k−1和2k ...Mi两段来分别讨论得出, 希望读者自己思考尝试一下。 这样就将第i种物品分成了O(logMi)种物品,将原问题转化为了复杂度 为O(V ΣlogMi)的01背包问题,是很大的改进。 下面给出O(logM)时间处理一件多重背包中物品的过程:

def MultiplePack(F,C,W,M) 
if C ·M ≥ V 
    CompletePack(F,C,W) 
    return k := 1 
while k < M 
    ZeroOnePack(kC,kW)
    M := M −k
    k := 2k 
    ZeroOnePack(C ·M,W ·M) 

3.4 O(VN)的算法
多重背包问题同样有O(V N)复杂度的算法。这个算法基于基本算法的状态 转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求 解。

https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1086

#include<stdio.h>
typedef long long ll;
template<class T>T max(T a,T b)
{
	return a>b?a:b;
}
ll f[60000];
ll w[2002000];
ll p[2002000];
int main()
{
	int n,W;
	scanf("%d%d",&n,&W);
	int k=0;
	for(int i=0;i<n;i++)
	{
		int w1,p1,c1;
		scanf("%d%d%d",&w1,&p1,&c1);
		int t=1;
		while(c1>0)
		{
			if(c1>t)
			{
				w[k]=t*w1;
				p[k]=t*p1;
			}
			else
			{
				w[k]=c1*w1;
				p[k]=c1*p1;
			}
			c1=c1-t;
			t=t*2;
			k++;
		}
	}
	for(int i=0;i<k;i++)
	for(int j=W;j>=w[i];j--)
	{
		f[j]=max(f[j],f[j-w[i]]+p[i]);
	}
	printf("%lld\n",f[W]);
	return 0;
}

例题:HDU 2191 

//HDU 2191 多重背包 
#include<stdio.h>
int val[110],wei[110],count[110],dp[110];//价格,重量,袋数。动态背包 
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n,m;// 钱数,种类 
		scanf("%d%d",&n,&m);
		for(int i=0;i<m;i++)
		{
			scanf("%d%d%d",val+i,wei+i,count+i);
		}
		for(int i=0;i<=n;i++) dp[i]=0;//初始为0 
		
		for(int i=0;i<m;i++) //第 i 种大米 
			for(int k=1;k<=count[i];k++) //大米 i 放入的次数 
				for(int j=n;j>=val[i];j--) // 动态规划 
					dp[j]=max(dp[j],dp[j-val[i]]+wei[i]);
		printf("%d\n",dp[n]); //数组dp存的是重量 
	}
	return 0;
}

5 二维费用的背包问题
5.1 问题
二维费用的背包问题是指:对于每件物品,具有两种不同的空间耗费,选 择这件物品必须同时付出这两种代价。对于每种代价都有一个可付出的最大值 (背包容量)。问怎样选择物品可以得到最大的价值。 设这两种代价分别为代价一和代价二,第i件物品所需的两种代价分别 为Ci和Di。两种代价可付出的最大值(两种背包容量)分别为V 和U。物品的 价值为Wi。
5.2 算法
费用加了一维,只需状态也加一维即可。设F[i,v,u]表示前i件物品付出两 种代价分别为v和u时可获得的最大价值。状态转移方程就是: F[i,v,u] = max{F[i−1,v,u],F[i−1,v−Ci,u−Di] + Wi} 如前述优化空间复杂度的方法,可以只使用二维的数组:当每件物品只可 以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的 循环,当物品有如多重背包问题时拆分物品。 这里就不再给出伪代码了,相信有了前面的基础,读者应该能够自己实现 出这个问题的程序。
5.3 物品总个数的限制
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能 取U件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的 件数费用均为1,可以付出的最大件数费用为U。换句话说,设F[v,u]表示付 出费用v、最多选u件时可得到的最大价值,则根据物品的类型(01、完全、多 重)用不同的方法循环更新,最后在f[0...V,0...U]范围内寻找答案。

6 分组的背包问题
6.1 问题
有N件物品和一个容量为V 的背包。第i件物品的费用是Ci,价值是Wi。这 些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品 装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
6.2 算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件 都不选。也就是说设F[k,v]表示前k组物品花费费用v能取得的最大权值,则 有: F[k,v] = max{F[k−1,v],F[k−1,v−Ci] + Wi |item i ∈ group k} 使用一维数组的伪代码如下:
 

for k = 1 to K 
    for v = V to 0 
        for item i in group k 
            F[v] = max{F[v],F[v−Ci] + Wi}

7 有依赖的背包问题
7.1 简化的问题


这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品i依赖于 物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物 品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多 件物品。


7.2 算法
这个问题由NOIP2006题目中金明的预算方案一题扩展而来。遵从该题的提 法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附 件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的 一个附件集合组成。 按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用 的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附 件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策 略。事实上,设有n个附件,则策略有2n + 1个,为指数级。

考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所 以一个主件和它的附件集合实际上对应于6中的一个物品组,每个选择了主件又 选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是 这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法, 因为物品组中的物品还是像原问题的策略一样多。 再考虑对每组内的物品应用2.3中的优化。我们可以想到,对于第k个物品组 中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,可 以对主件k的“附件集合”先进行一次01背包,得到费用依次为0...V − Ck所 有这些值时相应的最大价值Fk[0...V −Ck]。那么,这个主件及它的附件集合 相当于V −Ck + 1个物品的物品组,其中费用为v的物品的价值为Fk[v −Ck] + Wk,v的取值范围是Ck ≤ v ≤ V 。 也就是说,原来指数级的策略中,有很多策略都是冗余的,通过一次01背包 后,将主件k及其附件转化为V −Ck + 1个物品的物品组,就可以直接应用6的 算法解决问题了。


7.3 较一般的问题

更一般的问题是:依赖关系以图论中“森林”1的形式给出。也就是说,主 件的附件仍然可以具有自己的附件集合。限制只是每个物品最多只依赖于一个 物品(只有一个主件)且不出现循环依赖。 解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。 唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般 的01背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品 组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用 的附件所对应的价值。 事实上,这是一种树形动态规划,其特点是,在用动态规划求每个父节点 的属性之前,需要对它的各个儿子的属性进行一次动态规划式的求值。这已经 触及到了“泛化物品”的思想。看完8后,你会发现这个“依赖关系树”每一个 子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其 所有儿子的对应的泛化物品之和

附加:

01背包第k个最优解

        思想与01背包相同,不过在动态规划过程中,不可以把前一个状态的最优解扔掉,而要保存下来。

在01背包中,状态数组是 f [ v ] ,表示容积为v时的最优决策。而现在,我不仅要知道最优决策,我还想知道稍微差一点的决策,即第2决策、第3决策....排个名。f [ v ] 我们可以看做是 f [ v ] [ 1 ] 这样的二维数组,他的第二维只有一个元素,也就是最优决策。现在我们增大第二维,比如 f [ v ] [ 3 ] ,意思是,不仅保留了最优解,次解也保留下来了。

也可以理解为,第二维是一个集合,集合里就存了所有可能的决策,并按大小有序,排在第一的就是最优解。

现在问题是 第k个最优决策是多少。

           在01背包里,我们只保留了最优解,而把不是最优解的解直接舍弃了,即 

f[j]=max(f[j],f[j-w[i]]+v[i])

这时候,较小的那个解直接舍弃了,没保留下来。现在我们要做的就是,借助数组把所有的解都保留下来。

核心代码:

		int i,j,t;
		for(i=0;i<n;i++) //对每个物品扫描
			for(j=v;j>=vol[i];j--) //对每个状态进行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起来
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的循环相当于求a和b并集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}

例题:HDU2639

板子题)代码:

#include <stdio.h>  
#include<string.h>
int f[1010][33];//第一维和普通01背包一样。第二维存多个最优解
int val[110],vol[110];//价值和体积
int a[33],b[33];//用于暂时存储多组最优解 
int n,v,k;
int i,j,t,T;
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d",&n,&v,&k);
		for(i=0;i<n;i++) scanf("%d",&val[i]);
		for(i=0;i<n;i++) scanf("%d",&vol[i]);
		memset(f,0,sizeof(f));//对状态数组清0 
		
		for(i=0;i<n;i++) //对每个物品扫描
			for(j=v;j>=vol[i];j--) //对每个状态进行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起来
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的循环相当于求a和b并集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}
	printf("%d\n",f[v][k]);
	}
	return 0;
 } 

猜你喜欢

转载自blog.csdn.net/qq_37136305/article/details/81152503