算法篇——背包问题(子集树实现)

202 阅读4分钟

背包类型问题

一、最常见的0-1背包问题

问题描述:

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?

**这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。**这也许就是 0-1 背包这个名词的来历。

解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能

经典动态规划问题:

第一步要明确两点,「状态」和「选择」

先说状态,如何才能描述一个问题局面?只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题,对不对?所以状态有两个,就是「背包的容量」和「可选择的物品」

再说选择,也很容易想到啊,对于每件物品,你能选择什么?选择就是「装进背包」或者「不装进背包」嘛

明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

第二步要明确dp数组的定义。

首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。

dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]

比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。

根据这个定义,我们想求的最终答案就是dp[N][W]

int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            把物品 i 装进背包,
            不把物品 i 装进背包
        )
return dp[N][W]

如果你没有把这第**i个物品装入背包**,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。

如果你把这第**i个物品装入了背包**,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]

显然,你应该寻求剩余重量w-wt[i-1]限制下能装的最大价值,加上第i个物品的价值val[i-1],这就是装第i个物品的前提下,背包可以装的最大价值。

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            dp[i-1][w],
            dp[i-1][w - wt[i-1]] + val[i-1]
        )
return dp[N][W]

完整代码:

int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
    // vector 全填入 0,base case 已初始化
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i-1] < 0) {
                // 当前背包容量装不下,只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], 
                               dp[i - 1][w]);
            }
        }
    }

    return dp[N][W];
}

定义子问题 [公式] 为:在前 [公式] 个物品中挑选总重量不超过 [公式] 的物品,每种物品至多只能挑选1个,使得总价值最大;这时的最优值记作 [公式] ,其中 [公式][公式]

考虑第 [公式] 个物品,无外乎两种可能:选,或者不选。

  • 不选的话,背包的容量不变,改变为问题 [公式]
  • 选的话,背包的容量变小,改变为问题 [公式]

最优方案就是比较这两种方案,哪个会更好些:

[公式]

得到

[公式]

代码实现:(动态规划找最大价值,回溯寻找挑选的商品)

import java.util.*;

/**
 * @author SJ
 * @date 2020/11/28
 */
public class DP_0or1Package {
    //在背包可容纳的范围内装载的价值最大

    public static int N;//物品数量
    public static int Capacity;//背包容量
    public static int[] wt;//每个物品的重量
    public static int[] val;//每个物品的价值

    public static int[][] dp;
    public static void printDp(){
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[i].length; j++) {
                System.out.print(dp[i][j]);

            }
            System.out.println();
        }
    }



    static Scanner scanner = new Scanner(System.in);

    public DP_0or1Package(int num, int capacity) {

        N = num;
        Capacity = capacity;

        wt = new int[N];
        for (int i = 0; i < N; i++) {
            wt[i] = scanner.nextInt();
        }

        val = new int[N];
        for (int i = 0; i < N; i++) {
            val[i] = scanner.nextInt();
        }
        //dp[i][j]表示:前i个物品,当前的背包能装载的最大重量,可最大价值是dp[i][j]
        dp = new int[N + 1][Capacity + 1];

    }

    public static int knapsack() {


        for (int i = 1; i>=0&&i <= N; i++) {
            for (int j = 1; j>=0&&j <= Capacity; j++) {
                if (j < wt[i-1])//第i号物品装不下
                    dp[i][j] = dp[i - 1][j];
                else {

                    dp[i][j] = Math.max(dp[i - 1][j - wt[i - 1]] + val[i - 1], dp[i - 1][j]);

                }
            }

        }
        return dp[N][Capacity];
    }
    //从dp数组后往前回溯,寻找挑选的商品
    public static void backTrack(int i,int j,Stack<Integer> temp){
        while (i>0&&j>0){
            if (j<wt[i-1]){
                i--;
            }
            else if (j>=wt[i-1]&&dp[i][j]==dp[i-1][j-wt[i-1]]+val[i-1]){

                j=j-wt[i-1];
                temp.push(i-1);
                i--;
            }
            else if (j>wt[i-1]&&dp[i-1][j]==dp[i][j]){
                i--;
            }
        }


    }


    public static void main(String[] args) {
        int N=scanner.nextInt();
        int W=scanner.nextInt();
        new DP_0or1Package(N,W);
        int knapsack = knapsack();
        Stack<Integer> stack=new Stack<>();
        backTrack(N,Capacity,stack);
        System.out.println("背包所能容纳的做大价值为:"+knapsack);
        System.out.print("所选物品编号为:");
        while (!stack.empty()){
            System.out.print(stack.pop()+" ");
        }

    }


}

结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
3 4
2 1 3
4 2 3
背包所能容纳的做大价值为:6
所选物品编号为:0 1 
Process finished with exit code 0

方法二:

回溯法:穷举

回溯法

回溯法是一种非常有效的方法,有“通用的解题法”之称。它有点像穷举法,但是更带有跳跃性和系统性,他可以系统性的搜索一个问题的所有的解和任一解。回溯法采用的是深度优先策略。

​ 回溯法在确定了解空间的结构后,从根结点出发,以深度优先的方式搜索整个解空间,此时根结点成为一个活结点,并且成为当前的扩展结点。每次都从扩展结点向纵向搜索新的结点,当算法搜索到了解空间的任一结点,先判断该结点是否肯定不包含问题的解(是否还能或者还有必要继续往下搜索),如果确定不包含问题的解,就逐层回溯;否则,进入子树,继续按照深度优先的策略进行搜索。当回溯到根结点时,说明搜索结束了,此时已经得到了一系列的解,根据需要选择其中的一个或者多个解即可。

​ 回溯法解决问题一般分为三个步骤;

​ (1)针对所给问题,定义问题的解空间;

​ (2)确定易于搜索的解空间结构;

​ (3)以深度优先的方式搜索解空间。

segmentfault.com/a/119000001…

回溯法の解空间

就是要找出这个问题所有的解,在这些解里面筛选出最优解

对每一个物品判断是装还是不装,然后画出一个不规范的的图: 图片描述

在这个图中,连线旁边的数字:1表示装入背包中,0表示不装入背包。第一层中,我们判断是否放入A,放入就是1那条岔路,反之是0那条岔路,所以很直观地,我们就可以得到所有可能出现的8个情况:

{(1,1,1),(1,1,0),(1,0,1),(1,0,0),(0,1,1),(0,1,0),(0,0,1),(0,0,0)}

回溯法の公式

每种算法都可以选择递归和迭代的方式来写,显然这里用迭代方式来写是很麻烦的,可以说也没有人选择这样的方法,所以我们都采用递归来写。

子集树写法 我们的目的就是要遍历子集树上的每个解法,为了判断每一个解对不对,所以我们应该是深度遍历,所以我们需要:t(树的深度),数组x(每一层的结果选择)。先写出中心的遍历函数:

void backtrack(int t){
    if(t>=n){ 
           output();   //此时已到达叶子结点,用于输出一个结果
           return;
    }
    
    //对分叉点遍历0,1两种情况    
    for(int i=0;i<=1;i++){
        x[t]=i;  //给第t层代表的选择赋可能选择的值
        if(ok(t))  
            //ok是判断目前的一个解有可行的机会 
            backtrack(t+1);  
    }
        
}

此时我们应该注意,x数组的初始值应该都为0。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author SJ
 * @date 2020/12/5
 */
public class Knap_Backtrack {
    //回溯法解背包问题(dfs)
    public static int Capacity=12;//背包容量
    public static int N=4;//物品数量
    public static int[] weight=new int[]{8, 6, 2, 3};//weight[i]代表第i个物品的重量
    public static int[] val=new int[]{8, 6, 2, 3};//val[i]代表第i个物品的价值
    public static List<Integer> chosen;
    public static int Max_value=Integer.MIN_VALUE;//当前所作选择的最大价值

    public static void main(String[] args) {
        List<Integer> cs=new ArrayList<>();
        dfs(0,0,0,cs);
        System.out.println(Max_value);
        System.out.println(Arrays.toString(chosen.toArray()));


    }
    public static void dfs(int i,int curValue,int curWeight,List<Integer> curChosen){
        //搜索到排列树的第i层,判断第i个物品装还是不装
        if (i==N){//最后一层已经判断结束
            if (curValue>Max_value)
            {
                Max_value=curValue;
                chosen=new ArrayList<>(curChosen);
                
            }
            return;
        }
            int tempTotalValue=curValue+val[i];
            int tempTotalWeight=curWeight+weight[i];

            if (tempTotalWeight<=Capacity){
                curChosen.add(i);
                dfs(i+1,tempTotalValue,tempTotalWeight,curChosen);
                curChosen.remove(curChosen.size()-1);
;


            } 

            dfs(i+1,curValue,curWeight,curChosen);




    }

}
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
11
[0, 3]

Process finished with exit code 0