数据结构与算法Day33----动态规划(一)

一、0-1背包问题:

  假设背包的最大承载重量是9。我们有5个不同的物品,每个物品的重量分别是2, 2, 4, 6, 3。

1、采用回溯算法:

// 回溯算法实现。注意:我把输入的变量都定义成了成员变量。
private int maxW = Integer.MIN_VALUE; // 结果放到maxW中
private int[] weight = {2, 2, 4, 6, 3}; // 物品重量
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
public void f(int i, int cw) { // 调用f(0, 0)
    if (cw == w || i == n) { // cw==w表示装满了, i==n表示物品都考察完了
        if (cw > maxW) 
            maxW = cw;
        return;
    }
    f(i+1, cw); // 选择不装第i个物品
    if (cw + weight[i] <= w) {
        f(i+1,cw + weight[i]); // 选择装第i个物品
    }
}

2、采用递归树:

数据结构与算法Day33----动态规划(一)_第1张图片
递归树

  递归树中的每个节点表示一种状态,用(i, cw)来表示。其中, i表示将要决策第几个物品是否装入背包, cw表示当前背包中物品的总重量。比如,(2, 2)表示将要决策第2个物品是否装入背包,在决策前,背包中物品的总重量是2。
  有些子问题的求解是重复的,比如图中f(2, 2)和f(3,4)都被重复计算了两次。可以借助“备忘录”的解决方式,记录已经计算好的f(i, cw),当再次计算到重复的f(i, cw)的时候,可以直接从备忘录中取出来用,就不用再递归计算了,这样就可以避免冗余计算。

private int maxW = Integer.MIN_VALUE; // 结果放到maxW中
private int[] weight = {2, 2, 4, 6, 3}; // 物品重量
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
private boolean[][] mem = new boolean[5][10]; // 备忘录,默认值false
public void f(int i, int cw) { // 调用f(0, 0)
    if (cw == w || i == n) { // cw==w表示装满了, i==n表示物品都考察完了
    if (cw > maxW) 
        maxW = cw;
    return;
    }
    if (mem[i][cw]) return; // 重复状态
    mem[i][cw] = true; // 记录(i, cw)这个状态
    f(i+1, cw); // 选择不装第i个物品
    if (cw + weight[i] <= w) {
        f(i+1,cw + weight[i]); // 选择装第i个物品
    }
}

3、采用动态规划:

<1>、思路:

  1. 把整个求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。

  2. 把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过w个(w表示背包的承载重量),也就是例子中的9。于是就成功避免了每层状态个数的指数级增长。

  3. 用一个二维数组states[n][w+1],来记录每层可以达到的不同状态。

  4. 第0个(下标从0开始编号)物品的重量是2,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中物品的总重量是0或者2。用states[0][0]=true和states[0][2]=true来表示这两种状态。

  5. 第1个物品的重量也是2,基于之前的背包状态,在这个物品决策完之后,不同的状态有3个,背包中物品总重量分别是0(0+0), 2(0+2 or 2+0), 4(2+2)。用states[1][0]=true, states[1][2]=true, states[1][4]=true来表示这三种状态。

  6. 以此类推,直到考察完所有的物品后,整个states状态数组就都计算好了。图中0表示false, 1表示true。只需要在最后一层,找一个值为true的最接近w(这里是9)的值,就是背包中物品总重量的最大值。


    数据结构与算法Day33----动态规划(一)_第2张图片
    动态规划过程

<2>、代码:

思路一:
weight:物品重量, n:物品个数, w:背包可承载重量
public int knapsack(int[] weight, int n, int w) {
    boolean[][] states = new boolean[n][w+1]; // 默认值false
    states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
    states[0][weight[0]] = true;
    for (int i = 1; i < n; ++i) { // 动态规划状态转移
        for (int j = 0; j <= w; ++j) {// 不把第i个物品放入背包
            if (states[i-1][j] == true) 
                states[i][j] = states[i-1][j];
        }
        for (int j = 0; j <= w-weight[i]; ++j) {//把第i个物品放入背包
            if (states[i-1][j]==true) 
                states[i][j+weight[i]] = true;
        }
    }
    for (int i = w; i >= 0; --i) { // 输出结果
        if (states[n-1][i] == true) 
                return i;
    }
        return 0;
}
思路二:
public static int knapsack2(int[] items, int n, int w) {
    boolean[] states = new boolean[w+1]; // 默认值false
    states[0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
    states[items[0]] = true;
    for (int i = 1; i < n; ++i) { // 动态规划
        for (int j = w-items[i]; j >= 0; --j) {//把第i个物品放入背包
            if (states[j]==true) 
                states[j+items[i]] = true;
        }
    }
    for (int i = w; i >= 0; --i) { // 输出结果
        if (states[i] == true) 
            return i;
    }
    return 0;
}

二、0-1背包问题升级版:

  对于一组不同重量、不同价值、不可分割的物品,我们选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?

1、采用回溯算法:

private int maxV = Integer.MIN_VALUE; // 结果放到maxV中
private int[] items = {2, 2, 4, 6, 3}; // 物品的重量
private int[] value = {3, 4, 8, 9, 6}; // 物品的价值
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
public void f(int i, int cw, int cv) { // 调用f(0, 0, 0)
    if (cw == w || i == n) { // cw==w表示装满了, i==n表示物品都考察完了
        if (cv > maxV) 
            maxV = cv;
        return;
    }
    f(i+1, cw, cv); // 选择不装第i个物品
    if (cw + weight[i] <= w) {
        f(i+1,cw+weight[i], cv+value[i]); // 选择装第i个物品
    }
}

2、采用递归树:

  在递归树中,每个节点表示一个状态。现在需要3个变量(i, cw, cv)来表示一个状态。其中, i表示即将要决策第i个物品是否装入背包, cw表示当前背包中物品的总重量, cv表示当前背包中物品的总价值。

数据结构与算法Day33----动态规划(一)_第3张图片

  在递归树中,有几个节点的i和cw是完全相同的,比如f(2,2,4)和f(2,2,3)。在背包中物品总重量一样的情况下, f(2,2,4)这种状态对应的物品总价值更大,可以舍弃f(2,2,3)这种状态,只需要沿着f(2,2,4)这条决策路线继续往下决策就可以。也就是说, 对于(i, cw)相同的不同状态,那只需要保留cv值最大的那个,继续递归处理,其他状态不予考虑。

3、动态规划:

<1>、思路:

  把整个求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中。每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。
  用一个二维数组states[n][w+1],来记录每层可以达到的不同状态。不过这里数组存储的值不再是boolean类型的了,而是当前状态对应的最大总价值。把每一层中(i, cw)重复的状态(节点)合并,只记录cv值最大的那个状态,然后基于这些状态来推导下一层的状态。

<2>、代码:

public static int knapsack3(int[] weight, int[] value, int n, int w) {
    int[][] states = new int[n][w+1];
    for (int i = 0; i < n; ++i) { // 初始化states
        for (int j = 0; j < w+1; ++j) {
            states[i][j] = -1;
        }
    }
    states[0][0] = 0;
    states[0][weight[0]] = value[0];
    for (int i = 1; i < n; ++i) { //动态规划,状态转移
        for (int j = 0; j <= w; ++j) { // 不选择第i个物品
            if (states[i-1][j] >= 0) 
                states[i][j] = states[i-1][j];
        }
        for (int j = 0; j <= w-weight[i]; ++j) { // 选择第i个物品
            if (states[i-1][j] >= 0) {
                int v = states[i-1][j] + value[i];
                if (v > states[i][j+weight[i]]) {
                    states[i][j+weight[i]] = v;
                }
            }
        }
    }
    // 找出最大值
    int maxvalue = -1;
    for (int j = 0; j <= w; ++j) {
        if (states[n-1][j] > maxvalue) 
            maxvalue = states[n-1][j];
    }
    return maxvalue;
}

你可能感兴趣的:(数据结构与算法Day33----动态规划(一))