Backpack nine stresses personal finishing (construction

The theoretical basis of dynamic programming

Only when the problem in line with the principle of optimization and no aftereffect principle, is suitable for use dynamic programming

Principle of optimality

Optimization principles defined optimal strategy: regardless of past state and decision, the state of the previous decisions formed, the remaining decisions must constitute an optimal strategy .

Is simply a sub-optimal policy strategy (policy after produced) also must be the best , but all the local sub-optimal solution will lead to the whole question of global best

If a problem can meet the optimization principle, saying it has optimal substructure property, which is a prerequisite to determine whether the use of dynamic programming to solve the problem, if a problem can not satisfy the principle of optimality, then the problem is not suitable for dynamic plan to solve

for example:

A point on the board of the shortest distance to point B, then the sub-problem is to compute from point A to an intermediate point between the point B to the point B is the shortest distance

How to prove the principle of optimization it?

We assume that the shortest distance from point A to point C is d, it is assumed that the sub-optimal policy strategy through point B, referred to the policy from the point B to the point C is d1, point A to point B is the distanced2

By contradiction, assuming the presence of point B to point C the shortest distance d3, and d3 < d1, then d3 + d2 < d1 + d2 = d, which is the shortest distance d contradict therefore, d1is the shortest distance from point B to point C

As another counter-example:

All channels A to D requirements, the overall length of 4 divided by the minimum number of paths remainder obtained as the optimum path, an optimal seek path

As previously thought, it is the optimal value of A may be determined by the optimum value of B, and B is the optimal value of (3 + 5) mod 4 = 0, so it should be selected d2, and d6these two roads, in fact, the global optimal solution d4+d5+d6or d1+d5+d3, so here optimal solution to the problem of sub-optimal solution is not a problem that does not satisfy the principle of optimality, it is not suitable for use dynamic programming to solve the

No after-effect

Decision under certain state benefits, only related to the status and decision-making, regardless of the way to reach this state

Once the status of a stage, after which the evolutionary process had no influence and decision-making by the various states. In other words, the future has nothing to do with the past , the current state is a state of complete summary of previous history, history of previous decisions can only affect the future evolution by influencing the current state. In still other words, in the past it does not affect the choice of the optimal choice can do now, now can do the best choice only with the current state , regardless of the way through complex decisions on how to reach that state.

It is also important to question whether the method of dynamic programming can be used to answer authentication

We look back at the shortest path problem above, if the basis of the original plus a constraint: the same grid can only once . Well, the problem is not in line with no after-effect, because the solution will affect the selection strategy behind the front of a child problem child problem

01 backpack

There are N items and a capacity C of the backpack, the cost of the items i (the space / weight) are W [i] , is the value V [i] , each item only one can be selected or leave , which seek to make the sum of the items into the backpack maximum value

Suppose the total capacity of the backpack 10, five items, their value (v) and the weight (w) as follows:

Numbering 1 2 3 4
Value v 2 4 3 7
Weight w 2 3 5 5

Here is only one of each item, for each item, there are only two choices, take it or leave, denoted by 1 and 0, so called 01 backpack

Using xiselected to represent the i-th item ( xi = 1to, 0 represents not), virepresentative of the i-th value of the article, wion behalf of the i-th weight of the item, the initial state of our backpack with a capacity of 10, the total value of the package contents is 0 Next, we will begin to make a choice. No. 1 for the article, the current capacity of 10, 2 receiving its weight is more than enough, so there are two options, it is selected or not selected. We select an item when the backpack of capacity will be reduced, but the total value of the items inside will increase

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

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

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

分治法

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

  1. 抽象问题,背包问题抽象为寻找组合(x1,x2,x3…xn,其中xi = 0或1,表示第i个物品取或者不取),vi代表第i个物品的价值,wi代表第i个物品的重量,总物品数为n,背包容量为c
  2. 建模,问题即为max(x1×v1 + x2×v2 + x3×v3 + … + xn×vn)
  3. 约束条件,x1×w1 + x2×w2 + x3×w3 + … + 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

public class Solution{
    int[] vs = {0,2,4,3,7};
    int[] ws = {0,2,3,5,5};

    @Test
    public void testKnapsack1() {
        int result = ks(4,10);
        System.out.println(result);
    }

    private int ks(int i, int c){
        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 = Math.max(tmp1, tmp2);
        }
        return result;
    }
}

动态规划法

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

假设(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背包问题满足最优化

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

自上而下记忆法

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

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

public class Solution{
    int[] vs = {0,2,4,3,7};
    int[] ws = {0,2,3,5,5};
    Integer[][] results = new Integer[5][11];

    @Test
    public void testKnapsack2() {
        int result = ks2(4,10);
        System.out.println(result);
    }

    private int ks2(int i, int c){
        int result = 0;
        // 如果该结果已经被计算,那么直接返回
        if (results[i][c] != null) 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 = Math.max(tmp1, tmp2);
            results[i][c] = result;
        }
        return result;
    }
}

自下而上填表法

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

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

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

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

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

public class Solution{
    int[] vs = {0,2,4,3,7};
    int[] ws = {0,2,3,5,5};
    Integer[][] results = new Integer[5][11];
    
    @Test
    public void testKnapsack3() {
        int result = ks3(4,10);
        System.out.println(result);
    }

    private int ks3(int i, int j){
        // 初始化
        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];
    }
}

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

用子问题定义状态:用 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\j 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

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

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]);
    }

空间优化(滚动数组)

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

可以发现,每次求解 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}推导出来的,所以在第二轮循环扫描的时候应该由后往前进行计算,因为如果由前往后推导的话,前一次循环保存下来的值可能会被修改,从而造成错误。

这么说也许还是不太清楚,回头看上面的图,我们从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)的内容修改掉了,这样,我们后续计算就无法找到这个位置的原值,也就是上一轮循环中计算出来的值了,所以在遍历的时候,需要从后往前进行倒序遍历

public class Solution{
    int[] vs = {0,2,4,3,7};
    int[] ws = {0,2,3,5,5};
    int[] newResults = new int[11];

    @Test
    public void test() {
        int result = ksp(4,10);
        System.out.println(result);
    }

    private int ksp(int i, int c){
        // 开始填表
        for (int m = 0; m < vs.length; m++){
            int w = ws[m];
            int v = vs[m];
            for (int n = c; n >= w; n--){
                newResults[n] = Math.max(newResults[n] , newResults[n - w] + v);
            }
            // 可以在这里输出中间结果
            System.out.println(JSON.toJSONString(newResults));
        }
        return newResults[newResults.length - 1];
    }
}
[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件物品和一个容量为T的背包,第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]]+kv[i]) ,0\leq kw[i] \leq 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 种物品的总价值

public static class Knapsack {
    private static int[] P={0,5,8};
    private static int[] V={0,5,7};
    private static int T = 10;

    @Test
    public void soleve1() {
        int result = ks(P.length - 1,10);
        System.out.println("最大价值为:" + result);
    }

    private 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 tmp2 = ks(i-1, t - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        return result;
    }
}

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

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

最优化原理和无后效性

那这个问题可以不可以像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)

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

自上而下记忆法

public static class Knapsack {
    private static int[] P={0,5,8};
    private static int[] V={0,5,7};
    private static int T = 10;

    private Integer[][] results = new Integer[P.length + 1][T + 1];

    @Test
    public void solve2() {
        int result = ks2(P.length - 1,10);
        System.out.println("最大价值为:" + result);
    }

    private int ks2(int i, int t){
        // 如果该结果已经被计算,那么直接返回
        if (results[i][t] != null) return results[i][t];
        int result = 0;
        if (i == 0 || t == 0){
            // 初始条件
            result = 0;
        } else if(V[i] > t){
            // 装不下该珠宝
            result = ks2(i-1, t);
        } else {
            // 可以装下
            // 取k个物品,取其中使得价值最大的
            for (int k = 0; k * V[i] <= t; k++){
                int tmp2 = ks2(i-1, t - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        results[i][t] = result;
        return result;
    }
}

自下而上填表法

完全背包可以转化为01背包

多重背包

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

二维费用背包

Guess you like

Origin www.cnblogs.com/zhxmdefj/p/11128193.html