01,完全,多重,混合背包

动态规划理论基础

只有当问题符合最优化原理和无后效原理,才适合使用动态规划

最优化原理

最优化原理定义的最优策略:不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略

简单来说就是一个最优策略的子策略(之后产生的策略)也是必须是最优的,而所有子问题的局部最优解将导致整个问题的全局最优

如果一个问题能满足最优化原理,就称其具有最优子结构性质,这是判断问题能否使用动态规划解决的先决条件,如果一个问题不能满足最优化原理,那么这个问题就不适合用动态规划来求解

举个例子:

1536438-20190703181742106-236671025.png

棋盘上A点到B点的最短距离,那么子问题就是求从A点到B点之间的 中间点 到B点的最短距离

怎么证明最优化原理呢?

我们假设从A点到C点的最短距离为d,假设其最优策略的子策略经过B点,记该策略中B点到C点的距离为d1,A点到B点的距离为d2

用反证法,假设存在B点到C点的最短距离d3,并且d3 < d1,那么 d3 + d2 < d1 + d2 = d,这与d是最短距离相矛盾,所以,d1是B点到C点的最短距离

再举一个反例:

1536438-20190703181812063-550336470.png

求A到D的所有通道中,总长度除以4得到的余数最小的路径为 最优路径,求一条最优路径

按照之前的思路,A的最优取值应该可以由B的最优取值来确定,而B的最优取值为(3+5)mod 4 = 0,所以应该选d2和d6这两条道路,而实际上,全局最优解是d4+d5+d6或者d1+d5+d3,所以这里子问题的最优解并不是原问题的最优解,即不满足最优化原理,所以就不适合使用动态规划来求解了

无后效性

某状态下决策的收益,只与状态和决策相关,与到达该状态的方式无关

某个阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。换句话说,未来与过去无关,当前状态是此前历史状态的完整总结,此前历史决策只能通过影响当前的状态来影响未来的演变。再换句话说,过去做的选择不会影响现在能做的最优选择,现在能做的最优选择只与当前的状态有关,与经过如何复杂的决策到达该状态的方式无关

这也是用来验证问题是否可以使用动态规划来解答的重要方法

我们再回头看看上面的最短路径问题,如果在原来的基础上加上一个限制条件:同一个格子只能通过一次。那么, 这个题就不符合无后效性了,因为前一个子问题的解会对后面子问题的选择策略有影响

01背包

有N件物品和一个容量为C的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品仅有一件,可以选择放或不放,求将哪些物品装入背包可使价值总和最大

假设背包总容量为10,有5个物品,它们的价值(v)和重量(w)如下表:

编号1234
价值v2437
重量w2355

这里每个物品只有一个,对于每个物品而言,只有两种选择,要或不要,记为1和0,所以叫01背包

xi代表第i个物品的选择(xi = 1 要,0则代表不要),vi代表第i个物品的价值,wi代表第i个物品的重量,我们背包的初始状态是容量为10,包内物品总价值为0,接下来,我们就要开始做选择了。对于1号物品,当前容量为10,容纳它的重量2绰绰有余,因此有两种选择,选它或者不选。我们选择一个物品的时候,背包的容量会减少,但是里面的物品总价值会增加

1536438-20190703181834303-1407281389.png

那么对于物品2,当前剩余容量为8,大于物品2的容量3,因此也有两种选择,选或者不选

1536438-20190703181856276-2121047043.png

现在,我们得到了四个可能结果,我们每做出一个选择,就会将上面的每一种可能分裂成两种可能,后续的选择也是如此,最终,我们会得到如下的一张决策图

1536438-20190703181911297-1641847790.png

红色方框代表我们的最终待选结果,本来应该有16个待选结果,但有三个结果由于容量不足以容纳下最后一个物品,所以就没有继续进行裂变。然后,从这些结果中找出价值最大的,也就是13,这就是我们的最优选择,根据这个选择,依次找到它的所有路径,便可以知道该选哪几个物品

分治

接下来,我们就来分析一下,如何将它扩展到一般情况。为了实现这个目的,我们需要将问题进行抽象并建模,然后将其划分为更小的子问题,找出递推关系式

  1. 抽象问题,背包问题抽象为寻找组合(x1,x2,x3…xn,其中xi = 0或1,表示第i个物品取或者不取),vi代表第i个物品的价值,wi代表第i个物品的重量,总物品数为n,背包容量为c
  2. 建模,问题即为max(x1×v1 + x2×v2 + … + xn×vn)
  3. 约束条件,x1×w1 + x2×w2 + … + xn×wn < c
  4. 定义函数KS(i,j):代表当前背包剩余容量为j时,前i个物品最佳组合所对应的价值

对于第i个物品,有两种可能:

  1. 背包剩余容量不足以装下该物品,此时背包的价值与前i-1个物品的价值是一样的,KS(i,j) = KS(i-1,j)
  2. 背包剩余容量可以装下该物品,此时需要进行判断,因为装了该商品不一定能使最终组合达到最大价值,如果不装该商品,则价值为:KS(i-1,j),如果装了该商品,则价值为KS(i-1,j-wi) + vi,从两者中选择较大的那个,

所以可以得到递推关系式

KS(i,j)=KS(i-1,j)  //j<wi
KS(i,j)=max[KS(i-1,j),KS(i-1,j-wi)+vi]
  • 原问题是,将n件物品放入容量为c的背包
  • 子问题则是,将前i件物品放入剩余容量为j的背包,所得到的最优价值为KS(i,j)

如果只考虑第i件物品放还是不放,那么就可以转化为一个只涉及到前i-1个物品的问题

如果不放第i个物品,那么问题就转化为“前i-1件物品放入容量为j的背包中的最优价值组合”,对应的值为KS(i-1,j)

如果放第i个物品,那么问题就转化成了“前i-1件物品放入容量为j-wi的背包中的最优价值组合”,此时对应的值为KS(i-1,j-wi)+vi

#include <bits/stdc++.h>
using namespace std;

int ks(int i, int c)
{
    int vs[5] = {0,2,4,3,7};
    int ws[5] = {0,2,3,5,5};

    int result = 0;
    if (i == 0 || c == 0)
    {
        // 初始条件
        result = 0;
    }
    else if(ws[i] > c)
    {
        // 装不下该物品
        result = ks(i-1, c);
    }
    else
    {
        // 可以装下
        int tmp1 = ks(i-1, c);
        int tmp2 = ks(i-1, c-ws[i]) + vs[i];
        result = max(tmp1, tmp2);
    }
    return result;
}

int main()
{
    int result = ks(4,10);
    cout<<result<<endl;
}

动态规划

先来看看最优化原理。同样,我们使用反证法:

假设(x1,x2,…,xn)是01背包问题的最优解,则有(x2,x3,…,xn)是其子问题的最优解,假设(y2,y3,…,yn)是上述问题的子问题最优解,则有(v2y2+v3y3+…+vnyn)+v1x1 > (v2x2+v3x3+…+vnxn)+v1x1。说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优化

无后效性更好理解了。对于任意一个阶段,只要背包剩余容量和可选物品是一样的,那么我们能做出的现阶段的最优选择必定是一样的,是不受之前选择了什么物品所影响的。即满足无后效性

自上而下记忆法

与分治法的区别只是用一个二维数组用来存储计算的中间结果,减少重复计算,于是新建一个二维数组:

1536438-20190703181946151-420570359.png

表中每一个格子都代表一个子问题,我们最终的问题是求最右下角的格子的值,也就是i=4,j=10时的值。这里,我们的初始条件便是i=0或者j=0时对应的ks值为0

#include <bits/stdc++.h>
using namespace std;

int ks(int i, int c){
    int vs[5] = {0,2,4,3,7};
    int ws[5] = {0,2,3,5,5};
    int results[5][11]={0};

    int result = 0;
    // 如果该结果已经被计算,那么直接返回
    if (results[i][c] != 0)
        return results[i][c];
    if (i == 0 || c == 0){
        // 初始条件
        result = 0;
    }
    else if(ws[i] > c){
        // 装不下该物品
        result = ks(i-1, c);
    }
    else{
        // 可以装下
        int tmp1 = ks(i-1, c);
        int tmp2 = ks(i-1, c-ws[i]) + vs[i];
        result = max(tmp1, tmp2);
        results[i][c] = result;
    }
    return result;
}

int main()
{
    int result = ks(4,10);
    cout<<result<<endl;
}
自下而上填表法

接下来,我们用自下而上的方法来解一下这道题,思路很简单,就是不断的填表,回想一下上一篇中的斐波拉契数列的自下而上解法,这里将使用同样的方式来解决。还是使用上面的表格,我们开始一行行填表

1536438-20190703182002220-444120677.png

当i=1时,即只有物品1可供选择,那么如果容量足够的话,最大价值自然就是物品1的价值了

1536438-20190703182014372-137356876.png

当i=2时,有两个物品可供选择,此时应用上面的递推关系式进行判断即可。这里以i=2,j=3为例进行分析:

1536438-20190703182025324-1467617213.png

剩下的格子使用相同的方法进行填充即可

1536438-20190703182110643-1116897310.png

这样,我们就得到了最后的结果:13。根据结果,我们可以反向找出各个物品的选择,寻找的方法很简单,就是从i=4,j=10开始寻找,如果ks(i-1,j)=ks(i,j),说明第i个物品没有被选中,从ks(i-1,j)继续寻找。否则,表示第i个物品已被选中,则从ks(i-1,j-wi)开始寻找

1536438-20190703182129911-298653689.png

#include <bits/stdc++.h>
using namespace std;

int ks(int i, int c)
{
    int vs[5] = {0,2,4,3,7};
    int ws[5] = {0,2,3,5,5};
    int results[5][11]= {0};

    // 初始化
    for (int m = 0; m <= i; m++)
    {
        results[m][0] = 0;
    }
    for (int m = 0; m <= j; m++)
    {
        results[0][m] = 0;
    }
    // 开始填表
    for (int m = 1; m <= i; m++)
    {
        for (int n = 1; n <= j; n++)
        {
            if (n < ws[m])
            {
                // 装不进去
                results[m][n] = results[m-1][n];
            }
            else
            {
                // 容量足够
                if (results[m-1][n] > results[m-1][n-ws[m]] + vs[m])
                {
                    // 不装该珠宝,最优价值更大
                    results[m][n] = results[m-1][n];
                }
                else
                {
                    results[m][n] = results[m-1][n-ws[m]] + vs[m];
                }
            }
        }
    }
    return results[i][j];
}

int main()
{
    int result = ks(4,10);
    cout<<result<<endl;
}

动态规划里最关键的问题其实是寻找原问题的子问题,并写出递推表达式,只要完成了这一步,代码部分都是水到渠成的事情了

用子问题定义状态:用 f [i] [j] 表示已经处理到第 i 个物品,前 i 件物品放入一个剩余容量为 j 的背包可以获得的最大价值,那么会先出现两种情况

  1. 背包体积 j < i 的体积 w[i],这时候背包容量不足以放下第 i 件物品,只能选择不拿:f [i] [j] = f [i-1] [j]
  2. 背包体积 j >= i 的体积 w[i],这时候背包容量可以放下第 i 件物品,我们就要考虑是拿还是不拿:
    1. 拿: f [i] [j] = f [i-1] [j-w[i]]+v[i]; f [i-1] [j-w[i]] 指的是前 i-1 件物品,背包容量为j-w[i]时的最大价值(相当于为第i件物品腾出了w[i]的空间)
    2. 不拿:f [i] [j] = f [i-1] [j] 同1

所以可以得到状态转移方程

if (j<w[i]) //背包剩余容量j小于物品i的体积
    f[i][j] = f[i-1][j] //装不下第i个物体,目前只能靠前i-1个物体装包
else
    f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i])
f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])

i 件物品放入一个容量恰为 j 的背包可以获得的最大价值

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

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

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

i\j123456789101112
1000888888888
20008810101010181818
30668814141616181824
40669914141717191924
50669914141717192124
626891114161719192124

左上角按行求解,一直求解到右下角

for (int i = 1;i <= N;i++) //枚举物品
    for (int j = 0;j <= W;j++) { //枚举背包容量
        f[i][j] = f[i - 1][j];
        if (j >= w[i])
                f[i][j] = max(f[i-1][j],f[i-1][j-w[i]] + v[i]);
    }

例题HDU2602

分治和动态规划的区别

共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题,然后将子问题的解合并,形成原问题的解

不同点:

  • 分治法将分解后的子问题看成相互独立的,通过用递归来做
  • 动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做

空间优化(滚动数组)

递推本来就是用空间换时间,消耗的空间比较大

可以发现,每次求解 KS(i,j)只与KS(i-1,m) {m:1...j} 有关。也就是说,如果我们知道了K(i-1,1...j)就肯定能求出KS(i,j),如图

下一层只需要根据上一层的结果即可推出答案,举个栗子,看i=3,j=5时,在求这个子问题的最优解时,根据上述推导公式,KS(3,5) = max{KS(2,5),KS(2,0) + 3} = max{6,3} = 6;如果我们得到了i=2时所有子问题的解,那么就很容易求出i=3时所有子问题的解

因此,我们可以将求解空间进行优化,将二维数组压缩成一维数组,此时,装填转移方程变为:

KS(j) = max{KS(j),KS(j - wi) + vi}
这里KS(j - wi)就相当于原来的KS(i-1, j - wi)。需要注意的是,由于KS(j)是由它前面的KS(m){m:1..j}推导出来的,所以在第二轮循环扫描的时候应该由后往前进行计算,因为如果由前往后推导的话,前一次循环保存下来的值可能会被修改,从而造成错误

1536438-20190703182154280-1347518290.png

这么说也许还是不太清楚,回头看上面的图,我们从i=2推算i=3的子问题的解时,此时一维数组中存放的是{0,0,2,4,4,6,6,6,6,6,6},这是i=2时所有子问题的解,如果我们从前往后推算i=3时的解,比如,我们计算KS(0) = 0,KS(1) = KS(1) = 0 (因为j=1时,装不下三个物品,第三个物品的重量为5),KS(2)=2,KS(3)=4,KS(4)=4, KS(5) = max{KS(5), KS(5-5) + 3} = 6,....,KS(8) = max{KS(8),KS(8 - 5) + 3} = 7。在这里计算KS(8)的时候,我们就把原来KS(8)的内容修改掉了,这样,我们后续计算就无法找到这个位置的原值,也就是上一轮循环中计算出来的值了,所以在遍历的时候,需要从后往前进行倒序遍历

#include <bits/stdc++.h>
using namespace std;

int ks(int i, int c)
{
    int vs[5] = {0,2,4,3,7};
    int ws[5] = {0,2,3,5,5};
    int newResults[11]= {0};

    for (int m = 0; m < 5; m++)
    {
        int w = ws[m];
        int v = vs[m];
        for (int n = c; n >= w; n--)
        {
            newResults[n] = max(newResults[n], newResults[n - w] + v);
        }
        // 可以在这里输出中间结果
        for(int i =0;i<11;i++)
            printf("%d ",newResults[i]);
        printf("\n");
    }
    return newResults[11 - 1];
}

int main()
{
    int result = ks(4,10);
    cout<<result<<endl;
}
0 0 0 0 0 0 0 0 0 0 0
0 0 2 2 2 2 2 2 2 2 2
0 0 2 4 4 6 6 6 6 6 6
0 0 2 4 4 6 6 6 7 7 9
0 0 2 4 4 7 7 9 11 11 13
13

对于将来肯定用不到的数据,直接覆盖,所以这个方法叫滚动数组

缺点是牺牲了抹除了大量数据,不是每道题都可以用,但是在这里我们要的答案刚好是递推的最后一步,所以直接输出即可


先回顾我们之前的状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-W[i]] + [i]])

想知道f [i] [j] ,需要 f [i-1] [j] 和 f [i-1] [j-w[i]] ,我们之前是使用二维数组保存中间状态,所以可以直接取出这两个状态的值

我们可以直接使用一维数组 f [j] 表示:在执行 i 次循环后(已经处理 i 个物品),前 i 个物体放到剩余容量 j 时的最大价值,即之前的 f [i] [v]

与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[j] 一直在重复使用,所以,也会出现第i次循环可能会覆盖第 i - 1 次循环的结果

    for (int i = 1;i <= N;i++) //枚举物品  
        for (int j = W; j >= w[i]; j--) //枚举背包容量
            f[j] = max(f[j],f[j] - w[i]] + v[i]);

初始值

01背包问题一般有两种不同的问法,一种是“恰好装满背包”的最优解,要求背包必须装满,那么在初始化的时候,除了KS(0)0,其他的KS(j)都应该设置为负无穷大,这样就可以保证最终得到的KS(c)是恰好装满背包的最优解。另一种问法不要求装满,而是只希望最终得到的价值尽可能大,那么初始化的时候,应该将KS(0...c)全部设置为0

为什么呢?因为初始化的数组,实际上是在没有任何物品可以放入背包的情况下的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么都不装且价值为0的情况下被“恰好装满”,其他容量的背包均没有合法的解,因此属于未定义的状态,应该设置为负无穷大。如果背包不需要被装满,那么任何容量的背包都有合法解,那就是“什么都不装”。这个解的价值为0,所以初始状态的值都是0

完全背包

有N件物品和一个容量为C的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品都有无限件可用,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

贪心?把每种物品的价格除以体积来算出它们各自的性价比,然后只选择性价比最高的物品放入背包中

但这种做法还有一个问题,单个物品是无法拆分,不能选择半件,这样往往无法用性价比最高的物品来装满整个背包,比如背包空间为10,性价比最高的物品占用空间为7,那么剩下的空间该如何填充呢?

你可能还会想到用性价比第二高的物品填充,如果仍旧无法填满,那就依次用第三、第四性价比物品来填充,看似可行,但只需要举一个反例便能证明这个策略不可用:

  • 只有两个物品:A价值5,体积5;B价值8,体积7,背包容量为10

递归法

由01背包的

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

可以得到

f[i][j]=max [ (f[i−1][j−k*w[i]]+k*v[i]) ,0<=k*w[i]<=j ]

f [i] [j] 依然表示已经处理到第 i 个物品,前 i 件物品放入一个剩余容量为 j 的背包可以获得的最大价值

f [i−1] [j−k × w[i]] + k × v[i] 表示:i-1 种物品中选取若干件物品放入剩余空间为 j-k × w[i] 的背包中所能得到的最大价值 加上 k 件第 i 种物品的总价值

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2;

int P[maxn+1] = {0,5,8};
int V[maxn+1] = {0,5,7};
int C = 10;

int ks(int i, int t)
{
    int result = 0;
    if (i == 0 || t == 0)
    {
        // 初始条件
        result = 0;
    }
    else if(V[i] > t)
    {
        // 装不下该珠宝
        result = ks(i-1, t);
    }
    else
    {
        // 可以装下
        // 取k个物品i,取其中使得总价值最大的k
        for (int k = 0; k * V[i] <= t; k++)
        {
            int temp = ks(i-1, t - V[i] * k) + P[i] * k;
            if (temp > result)
            {
                result = temp;
            }
        }
    }
    return result;
}
int main()
{
    int result = ks(maxn, 10);
    printf("%d",result);
}

对比一下01背包问题中的递归解法,就会发现唯一的区别便是这里多了一层循环,因为01背包中,对于第i个物品只有选和不选两种情况,只需要从这两种选择中选出最优的即可,而完全背包问题则需要在k种选择中选出最优解,这便是最内层循环在做的事情

for (int k = 0; k * V[i] <= t; k++){
    // 选取k个第i件商品的最优价值为tmp2
    int temp = ks(i-1, t - V[i] * k) + P[i] * k;
    if (temp > result){
        // 从中拿出最大的值即为最优解
        result = temp;
    }
}

最优化原理和无后效性

那这个问题可以不可以像01背包问题一样使用动态规划来求解呢?来证明一下即可。

首先,先用反证法证明最优化原理:假设完全背包的最优解为F(n1,n2,…,nN)(n1,n2 分别代表第1、第2件物品的选取数量),完全背包的子问题为,将前i种物品放入容量为t的背包并取得最大价值,其对应的解为:F(n1,n2,…,ni),假设该解不是子问题的最优解,即存在另一组解F(m1,m2,…,mi),使得F(m1,m2,…,mi) > F(n1,n2,…,ni),那么F(m1,m2,…,mi,…,nN) 必然大于 F(n1,n2,…,nN),因此 F(n1,n2,…,nN) 不是原问题的最优解,与原假设不符,所以F(n1,n2,…,ni)必然是子问题的最优解

再来看看无后效性:前i种物品如何选择,只要最终的剩余背包空间不变,就不会影响后面物品的选择,满足无后效性

因此,完全背包问题也可以使用动态规划来解决。

动态规划

ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)

递推法中,已经找到了递推关系式,就是状态转移方程

自上而下记忆法
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2;

int P[maxn+1] = {0,5,8};
int V[maxn+1] = {0,5,7};
int C = 10;
int results[maxn+1][10]={0};

int ks(int i, int t)
{
// 如果该结果已经被计算,那么直接返回
    if (results[i][t] !=0)
        return results[i][t];
    int result = 0;
    if (i == 0 || t == 0)
    {
        // 初始条件
        result = 0;
    }
    else if(V[i] > t)
    {
        // 装不下该珠宝
        result = ks(i-1, t);
    }
    else
    {
        // 可以装下
        // 取k个物品,取其中使得价值最大的
        for (int k = 0; k * V[i] <= t; k++)
        {
            int temp = ks(i-1, t - V[i] * k) + P[i] * k;
            if (temp > result)
            {
                result = temp;
            }
        }
    }
    results[i][t] = result;
    return result;
}
int main()
{
    int result = ks(maxn, 10);
    printf("%d",result);
}
自下而上填表法

1536438-20190705235421674-158358529.png

1536438-20190705235413519-2082486264.png

这里当t=10时,因为最多只能放得下1个i2物品,所以只需要将两个数值进行比较,如果t=14,那么就需要将取0个、1个和两个i2物品的情况进行比较,然后选出最大值。

for (int i = 1; i <= maxn; i++)
    for (int j = 0; j < C; j++)
        for (int k = 0; k * V[i] <= j; k++)
            KS[i][j] = max(KS[i][j], KS[i-1][j-k * V[i]] + k * P[i]);

跟01背包问题一样,完全背包的空间复杂度也可以进行优化

优化后的状态转移方程为:

#include <bits/stdc++.h>
using namespace std;
const int n = 2;

int P[n+1] = {0,5,8};
int V[n+1] = {0,5,7};
int C = 10;
int results[11]={0};

int main()
{

    for(int i=1; i<=n; i++)
        for(int j=P[i]; j<=C; j++)//注意此处,与0-1背包不同,这里为顺序,0-1背包为逆序
            results[j]=max(results[j],results[j-P[i]]+V[i]);
    printf("%d",results[C]);
}

输出如下:

[0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,5,5,5,5,5,10]
[0,0,0,0,0,5,5,8,8,8,10]
10

其实完全背包问题也可以转化成01背包问题来求解,因为第i件物品最多选 C/Vi(向下取整) 件,于是可以把第i种物品转化为C/Vi件体积和价值相同的物品,然后再来求解这个01背包问题。具体方法这里就不多说了,留给大家自行解决。如果遇到问题,可以翻开前面关于01背包问题的两篇文章。

转化为01背包

先复习一下01背包状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-W[i]]+V[i])
for (i = 1; i <= n; i++) //n为物品个数
    for (j = 1; j <= C; j++) //C为背包总容量
        if (j < C[i])
            KS[i][j] = KS[i - 1][j];
        else
            KS[i][j] = max(KS[i - 1][j], KS[i - 1][j - C[i]] + v[i]);

然后我们看到完全背包的初级状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-k*W[i]]+k*V[i])(1 <= k <= C/W[i])
    f[i][j] = max(f[i-1][j-k*W[i]]+k*V[i])                    (0 <= k <= C/W[i])

把 k=0 拿出来单独考虑,即比较在不放第i种物品放第i种物品k件(k>=1)中结果最大的那个k这两种情况下谁的结果更大

f[i][j] = max(f[i-1][j], max(f[i-1][j-k*W[i]]+k*V[i]) )           (k >= 1)

考虑上式放第i种物品这种情况:放的话至少得放1件,先把这确定的1件放进去,即:在第i件物品已经放入1件的状态下再考虑放入k(k>=0)件这种物品的结果是否更大(如果k=1,说明第i种物品放了2件,因为前提状态是必然有一件物品已经放入)

f[i][j] = max( f[i-1][j], max( f[i-1][(j-W[i])-k*W[i]]+k*V[i] )+V[i] )  (k >= 0)

结合之前蓝色的式子,可以发现,上式的后半部分就等于f [i] [j - w[i]] + v[i],于是得出最终状态转移方程:

    f[i][j] = max(f[i-1][j], f[i][j-W[i]]+V[i]) 

例题HDU1114

多重背包

有N件物品和一个容量为的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品只有 n[i] 件可用,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

对比一下完全背包,其实只是多了一个限制条件,完全背包问题中,物品可以选择任意多件,只要你装得下,装多少件都行,但多重背包就不一样了,每种物品都有指定的数量限制,所以不是你想装,就能一直装的。

举个栗子:有A、B、C三种物品,相应的数量、价格和占用空间如下图:

物品数量价值体积
A423
B334
C245

递归法

用ks(i,j)表示前i种物品放入一个容量为 j 的背包获得的最大价值,那么对于第 i 种物品,我们有 k 种选择,0 <= k <= M[i] && 0 <= k * V[i] <= j,即可以选择0、1、2…M[i]个第i种物品,所以递推表达式为:

ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= j)

同时,ks(0,j)=0;ks(i,0)=0;

对比一下完全背包的递推关系式:

ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= j)

简直一毛一样,只是k多了一个限制条件而已。

    int ks(int i, int j){
        int result = 0;
        if (i == 0 || j == 0){
            // 初始条件
            result = 0;
        } else if(V[i] > j){
            // 装不下该珠宝
            result = ks(i-1, j);
        } else {
            // 可以装下
            // 取k个物品i,取其中使得总价值最大的k
            for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
                int tmp2 = ks(i-1, j - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        return result;
    }

完全背包中的递归解法:

int ks(int i, int j){
    int result = 0;
    if (i == 0 || j == 0){
        // 初始条件
        result = 0;
    } else if(V[i] > j){
        // 装不下该珠宝
        result = ks(i-1, j);
    } else {
        // 可以装下
        // 取k个物品i,取其中使得总价值最大的k
        for (int k = 0; k * V[i] <= j; k++){
            int tmp2 = ks(i-1, j - V[i] * k) + P[i] * k;
            if (tmp2 > result){
                result = tmp2;
            }
        }
    }
    return result;
}

仅仅多了一个判断条件k <= M ,所以只要弄懂了完全背包,多重背包就不值一提了。

最优化原理和无后效性的证明跟多重背包基本一致,所以就不重复证明了

动态规划

自上而下记忆法
ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= j)
    int ks2(int i, int j){
        // 如果该结果已经被计算,那么直接返回
        if (results[i][t] != 0) return results[i][t];
        int result = 0;
        if (i == 0 || j == 0){
            // 初始条件
            result = 0;
        } else if(V[i] > j){
            // 装不下该珠宝
            result = ks2(i-1, j);
        } else {
            // 可以装下
            // 取k个物品,取其中使得价值最大的
            for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
                int tmp2 = ks2(i-1, j - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        results[i][t] = result;
        return result;
    }
自下而上填表法
        for (int i = 0; i < n; i++){
            for (int j = 0; j <= C; j++){
                for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
                    dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
                }
            }
        }

优化后:

ks(j) = max{ks(j), ks(j - Vi) + Pi}
#include <bits/stdc++.h>
using namespace std;
const int n = 3;

int P[n+1] = {2,3,4};
int V[n+1] = {3,4,5};
int M[n+1] = {4,3,2};
int C = 15;
int newResults[16] ;

int ksp(int i, int j)
{
    // 开始填表
    for (int m = 0; m < i; m++)
    {
        // 考虑第m个物品
        // 分两种情况
        // 1: M[m] * V[m] > C 则可以当做完全背包问题来处理
        if (M[m] * V[m] >= C)
        {
            for (int n = V[m]; n <= j ; n++)
            {
                newResults[n] = max(newResults[n], newResults[n - V[m]] + P[m]);
            }
        }
        else
        {
            // 2: M[m] * V[m] < C 则需要在 newResults[n-V[m]*k] + P[m] * k 中找到最大值,0 <= k <= M[m]
            for (int n = V[m]; n <= j ; n++)
            {
                int k = 1;
                while (k < M[m] && n > V[m] * k )
                {
                    newResults[n] = max(newResults[n], newResults[n - V[m] * k] + P[m] * k);
                    k++;
                }
            }
        }
        // 可以在这里输出中间结果
    }
    return newResults[C];
}

int main()
{
    int result = ksp(n+1,C);
    printf("%d",result);
}
[0,0,0,0,2,2,2,4,4,4,6,6,6,8,8,8]
[0,0,0,0,2,3,3,4,5,6,6,7,8,9,9,10]
[0,0,0,0,2,3,4,4,5,6,7,8,8,9,10,11]
11

这里有一个较大的不同点,在第二层循环中,需要分两种情况考虑,如果 M[m] * V[m] >= C ,那么第m个物品就可以当做完全背包问题来考虑,而如果 M[m] * V[m] < C,则每次选择时,需要从 newResults[n-V[m]*k] + P[m] * k(0 <= k <= M[m])中找到最大值

1536438-20190705235320195-1187324199.png

例题HDU2844

01,完全,多重 总结

回顾一下三个背包问题的定义:

01背包:
有N件物品和一个容量为V的背包,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。

完全背包:
有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。

多重背包:
有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Ci,价值为Wi,求解入哪些物品可以使得背包中总价值最大。

三种背包问题都有一个共同的限制,那就是背包容量,背包的容量是有限的,这便限制了物品的选择,而三种背包问题的共同目的,便是让背包中的物品价值最大。

不同的地方在于物品数量的限制,01背包问题中,每种物品只有一个,对于每种物品而言,便只有选和不选两个选择。完全背包问题中,每种物品有无限多个,所以可选的范围要大很多。在多重背包问题中,每种物品都有各自的数量限制。

三种背包问题虽然对于物品数量的限制不一样,但都可以转化为01背包问题来进行思考。在完全背包问题中,虽然每种物品都可以选择无限个,但由于背包容量有限,实际上每种物品可以选择的数量也是有限的,那么将每种物品都看做是 V/Ci 种只有一件的不同物品,不就成了01背包问题吗?对于多重背包也是如此,只是每种物品的膨胀数量变成了 min{Mi, V/Ci}。

所以说,01背包问题是所有背包问题的基础,弄懂了01背包问题后,完全背包和多重背包就没有什么难的地方了。

下面我们来对比一下三种背包问题的状态转移方程,以便更好的理解它们之间的联系:

01背包的状态转移方程:

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

完全背包的状态转移方程:

F[i,v] = max{F[i-1,v-kCi] + kWi | 0 <= kCi <= v}

多重背包的状态转移方程:

F[i,v] = max{F[i-1,v-kCi] + kWi | 0 <= k <= Mi}

把这三个方程放到一起,便能很清晰的看到它们之间的关系了,三种背包问题都是基于子问题来选取价值最大的一个,只是选择的范围不一样。

01背包考虑的是选和不选,所有只需要比较两种策略的最大值即可,而完全背包和多重背包要考虑的是选几个的问题。

这样说也许还是不够形象,举个栗子就能比较好的说明了:

假设背包容量为10,有两个物品可选,价值分别为:3,2,容量占用分别为,4,3。

假设背包容量为10,有两个物品可选,价值分别为:3,2,容量占用分别为,4,3。

序号 i费用 P价值 V
132
323

初始状态

1536438-20190705235247095-1207212953.png

01背包填表:

1536438-20190705235235730-631918605.png

完全背包填表:

1536438-20190705235219482-624390945.png

多重背包填表:

1536438-20190705235206740-1418699536.png

下面再来看看三种背包问题的一维数组解决方案

N代表物品数量,wi代表第i个物品占用的容量,C代表背包总容量,vi代表第i个物品的价值,Mi表示最多可选数量

01背包:

for i <- 1 to N
    for j <- C to wi
        F[j] = max{F[j],F[j-wi] + vi}

将其核心部分抽象出来:

def ZeroOneKnapsack(F,wi,W)
    for j <- C to wi
        F[j] = max{F[j],F[j-wi] + W}

则01背包问题可以表示为:

for i <- 1 to N
    ZeroOneKnapsack(F,wi,vi)

完全背包:

for i <- 1 to N
    for j <- wi to C
        F[j] = max{F[j],F[j-wi] + vi}

将其核心部分抽象出来:

def CompleteKnapsack(F,wi,W)
    for j <- wi to C
        F[j] = max{F[j],F[j-wi] + W}

则完全背包问题的解可以表示为:

for i <- 1 to N
    CompleteKnapsack(F,wi,vi)

多重背包:

for i <- 1 to N
    if j < wi * Mi
        F[j] = max{F[j],F[j-wi] + vi}
    else
        for j <- wi to C
            k <- 1
            while k < M && j > wi * k
                F[j] = max{F[j],F[j-wi*k] + vi*k}
                k++

抽象出核心逻辑:

def MultiKnapsack(F,wi,W,M)
    if wi * M >= C
        CompleteKnapsack(F,wi,W)
        return
    else
        k <- 1
        while k < M
            ZeroOneKnapsack(F,KC,KW)
            k++
        return

则多重背包问题的解可以表示为:

for i <- 1 to N
    MultiKnapsack(F,wi,vi,Mi)

混合背包

现在我们来考虑一种更为复杂的情况,如果可选的物品同时具有上述三种特性,即:有的物品只能选一个,有的物品可以选择任意多个,有的物品只能选择有限多个,那么此时该如何决策呢?

回顾一下上面的三种背包问题的抽象解,就会发现他们每次都只会考虑一种物品,区别只在于第i个物品的可选策略。所以对于混合背包问题,同样也可以一个一个物品考虑,如果这个物品是最多选一个,那么就采用01背包的解决策略,如果是可以选择任意多个,那么就使用完全背包的解决策略,如果只能选择有限多个,那么就使用多重背包的解决策略

伪代码如下:

for i <- 1 to N
    if 第i件物品属于01背包
        ZeroOneKnapsack(F,Ci,Wi)
    else if 第i件物品属于完全背包
        CompleteKnapsack(F,Ci,Wi)
    else if 第i件物品属于多重背包
        MultiKnapsack(F,Ci,Wi,Mi)

三种背包问题都是基于子问题来选取价值最大的一个,只是选择的范围不一样。

01背包考虑的是选和不选,所有只需要比较两种策略的最大值即可,而完全背包和多重背包要考虑的是选几个的问题

转载于:https://www.cnblogs.com/zhxmdefj/p/11128193.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值