从01背包到动态规划

1.问题-原始01背包问题

有n件物品(不可分割), 每件物品的价值为nCost[i],体积为nVol[i],要放入到总容积为kBag的背包中,输出背包中能够装载物品的最大价值。

例:有5个物品(忽略每个数组的第一个值)

n=5
nCost[] = {0 , 2 , 5 , 3 , 10 , 4}
nVol[] = {0 , 1 , 3 , 2 , 6 , 2}
kBag = 12

输出:
21

21是选择编号为1,3,4,5的物品所产生的最大值。

1.2求解

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

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

用二维数组dp[i][j]来储存当决策到第i件物品时,背包容积剩余j的情况下背包中物品价值的最大值。按照每件物品依次决策,容易得到当j < nVol[i]时,背包剩余容积小于要决策的物品体积,只能放弃装入;而j >= nVol[i]时,可以选择装入也可以选择不装入,按照前述规则,此处应该取两者最大值,得到核心逻辑:

if (j < nVol[i]) {
	dp[i][j] = dp[i-1][j];//放弃装入
} else {
	dp[i][j] = max(dp[i-1][j], dp[i-1][j-nVol[i]] + nCost[i]);//表示装入物品,容积变为j-nVol[i],价值增加nCost[i].
}

代码如下:

public class ZeroOnePackage {

    int clacDp(int[] nCost, int[] nVol, int kBag) {
        int[][] dp = new int[7][13];

        for (int i = 1; i < 6; i++) {
            for (int j = 1; j <= kBag; j++) {
                if (j < nVol[i]) {
                    //剩余容量小于物品容量 丢弃
                    dp[i][j] = dp[i-1][j];
                } else {
                    //状态转移方程, 不装入情况和装入情况选最大值
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-nVol[i]] + nCost[i]);
                }
                System.out.println("dp["+i+"]["+j+"]:"+dp[i][j]);
            }
        }
        return dp[5][12];
    }

    public static void main(String[] args) {

        /**
         * 物体价值(第一个为哨兵)
         */
        int nCost[] = new int[]{0 , 2 , 5 , 3 , 10 , 4};
        /**
         * 物体体积(第一个为哨兵)
         */
        int nVol[] = new int[]{0 , 1 , 3 , 2 , 6 , 2};
        /**
         * 背包容积
         */
        int kBag = 12;

        System.out.println(new ZeroOnePackage().clacDp(nCost, nVol, kBag));
    }
}

1.3分析

令物品件数为n,背包容积为w。
空间复杂度:O(n*w),时间复杂度:O(n*w).
尽管比起回溯算法的O(2^n)的时间复杂度小了很多,但是额外申请了大小为n*w的数组,因此耗费了空间,属于空间换时间的操作。
能不能把空间优化一下呢?

开辟一维数组states[w]来替代dp[i][w],dp[i][w]是由dp[i-1][w]dp[i-1][w-nVol[i]两个问题递推而来,我们需要保证递推顺序,保证我们的states[w]不是从dp[i][w-nVol[i]]递推而来。

当计算dp[i][j]的时候,由于dp[i][j+1…w]的状态已经被计算过了,所以dp[i-1][j…w]的值已经没有用了。所以dp[i][j]的值可以存放在dp[i-1][j]的位置。
由于dp[i][j] = max(dp[i-1][j], dp[i-1][j-nVol[i]] + nCost[i]),dp[i][j]改变的值只与dp[i-1][1…j]有关,并且dp[i-1][1…j]是上一次循环保存下来的值,我们逆序循环,上一次循环得到的缓存等于states[i],新的states[i]遵循原有的状态转移方程,就有states[i] = max(states[i], states[w-nVol[i]] + nCost[i]).

根据状态转移逻辑有

states[i] = max(states[i], states[w-nVol[i]] + nCost[i])

如果我们顺序遍历,则先会确定i较小时的states[i]的值,这样的话,如果遍历到i较大时的states[i],使用了states[w - nVol[i]]的值,就会出错,原因就是states[i]并非由dp[i-1][w-nVol[i]]递推而来,而是从dp[i][w-nVol[i]]递推而来。

假定我们已经确定states[2] = 7,相当于把i=1,2的物品放入了背包,但是我们用states[2]来确定states[4]的时候,由于i=1已经放入了背包,和我们预想的算法不同.

代码如下:

public class ZeroOnePackage {
.
    int clacDp2(int[] nCost, int[] nVol, int kBag) {
        int[] states = new int[13];

        for (int i = 1; i < 6; i++) {
            for (int j = kBag; j >= nVol[i]; j--) {
                states[j] = Math.max(states[j], states[j-nVol[i]] + nCost[i]);
            }
        }
        return states[12];
    }

    public static void main(String[] args) {

        /**
         * 物体价值(第一个为哨兵)
         */
        int nCost[] = new int[]{0 , 2 , 5 , 3 , 10 , 4};
        /**
         * 物体体积(第一个为哨兵)
         */
        int nVol[] = new int[]{0 , 1 , 3 , 2 , 6 , 2};
        /**
         * 背包容积
         */
        int kBag = 12;

        System.out.println(new ZeroOnePackage().clacDp2(nCost, nVol, kBag));
    }
}

如果想要保存最优解选择的物品序号,再开辟一个数组,然后通过二维数组的方式回溯,暂未找到一维数组回溯的解决方案。

1.4思考题

假定一个类似杨辉三角形的结构,每个节点有一个值,每个节点只能到达下一层的左或右节点,求顶点到最底层点路径经过所有节点之和的最小值。

输入第一行节点层数n,2-n+1行为节点的值,输出节点的和的最小值。

例:
输入:
5
1
2,3
4,5,6
7,8,9,10

输出:
20

令dp[i][j]为第i层第j个节点的决策
状态转移方程:dp[i][j] = max(dp[i-1][j-1] +tri[i][j], dp[i-1][j]+tri[i][j]),考虑左右边际情况(最左侧节点只能到上一层最左侧节点,最右侧节点只能到上一层最右侧节点)

代码如下:

public class Triangle {

    static int solution(int[][] tri, int n) {
        int[][] dp = new int[n+1][n+1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                if (j > 1 && j < i) {
                    dp[i][j] = Math.max(dp[i-1][j-1] + tri[i][j], dp[i-1][j] + tri[i][j]);
                } else if (j < i) {
                    dp[i][j] = dp[i-1][j] + tri[i][j];
                } else {
                    dp[i][j] = dp[i-1][j-1] + tri[i][j];
                }
            }
        }
        int max = 0;
        for (int j = 1; j <= n; j++) {
            max = Math.max(max, dp[n][j]);
        }
        return max;
    }

    public static void main(String[] args) {
        int n = 4;
        int[][] tri = new int[5][5];
        for (int i = 1; i < 5; i++) {
            for (int j = 1; j <= i; j++) {
                tri[i][j] = (i-1)*i/2+j;
            }
        }
        System.out.println(solution(tri, n));
    }

}

2. 动态规划理论

2.1 什么样的问题适合动态规划

”一个模型三个特征”。

一个模型:适合动态规划的模型。

三个特征:最优子结构、无后效性、重复子问题

最优子结构:通过子问题的最优解推导出问题的最优解,即后面阶段的状态可以通过前面阶段的状态推导出来。

无后效性:推导一个前置状态,不需要知道前置状态是如何推导出来的;一个状态一旦被推导出,就不受后续状态的影响。满足动态规划特征的问题基本都满足无后效性。

重复子问题:不同的决策序列进行到某一步时可能会产生相同的状态。

2.2 动态规划的解题思路总结

2.2.1 状态转移表法

一般DP能够解决的问题,回溯法也能解决,通过回溯画出递归决策树,从中找出重复子问题,再查看是否能用DP解决。

用回溯+重复子问题缓存的方法,一般时间复杂度接近DP,或者采用状态转移表法。

适合最高二维的状态表,根据决策过程将对应位置的状态填充到表中,最后经过分析翻译成代码。三维以上的问题不适合采用状态表。

2.2.2 状态转移方程法

分析子问题的递归求解法,接下来分析递推公式,通过递归子问题+记忆化或者递推来解决问题。

实际上状态转移方程写出来基本问题就解决了。

2.2.3 思考题

假设我们有几种不同币值的硬币 v1,v2,……,vn(单位是元)。如果我们要支付 w 元,求最少需要多少个硬币。比如,我们有 3 种不同的硬币,1 元、3 元、5 元,我们要支付 9 元,最少需要 3 个硬币(3 个 3 元的硬币)。

输入:第一行硬币的面值,第二行要支付的金额
1,2,5,10,20,50,100
1203

输出:需要硬币最少的数量
14

选择12个100,1个1,1个2

代码如下:

public class CoinChange {

    static int solution(int[] v, int w) {
        if (w == 0) {
            return 0;
        }
        int[] dp = new int[w+1];
        for (int i = 1; i <= w; i++) {
            dp[i] = i;
        }
        for (int i = 1; i <= w; i++) {
            for (int j = 1; j < v.length; j++) {
                if (i >= v[j] && dp[i-v[j]] + 1 < dp[i]) {
                    dp[i] = dp[i-v[j]] + 1;
                }
            }
        }
        return dp[w];
    }

    public static void main(String[] args) {
        int[] v = new int[]{0,1,2,5,10,20,50,100};
        int w = 1203;
        System.out.println(solution(v,w));

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值