动态规划


一、动态规划算法介绍

  1. 核心思想:将大问题分成小问题,进而一步步获取最优解的处理算法
  2. 与分治法类似,先解决子问题,再从子问题的解中得到问题的解
  3. 与分治法不同的是,适合于动态规划的问题,分解后的子问题往往不是相互独立的(即下阶段求解是建立在上个阶段的解的基础上)。
  4. 可通过填表的方式逐步推进
  5. 公式OPT(i) = max(选i,不选i)

二、背包问题

有一个背包,容量为4磅,现有如下物品

物品 重量 价格
吉他(G) 1 1500
音响(S) 4 3000
电脑(L) 3 2000

要求:

  1. 装入物品达到总价值最大,且重量不超出
  2. 装入的物品不能重复

分析:

  • 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
  • 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

三、问题解决

物品(i)\容量(j) 0 1 2 3 4
0 0 0 0 0
吉他1500(1) 0 1500 1500 1500 1500
音响3000(4) 0 1500 1500 1500 3000
电脑2000(3) 0 1500 1500 2000 3500

table[i][j]表示前i个物品能装入容量为j的背包的最大价值。

w[i]表示第i的物品的重量。

v[i]表示第i个物品的价值。

问题解决思路:从上到下遍历物品,先排除未被遍历的物品。如当背包容量为4的情况下,遍历到物品吉他,判断吉他的重量是否大于背包的重量,如果大于,则将table[1][4]的值置为上一个单元格的值,即table[0][4]。如果小于,比较table[0][4]和放入吉他后+剩余空间(0)的最大价值。在继续往下遍历,以此类推,最后table[maxi][4]即是要求的最大价值。

实际上,该问题的求解主要用到容量为4的这一列,其他列使用来填充剩余空间的

公式:

  1. i=0j=0,即table[i][0]或table[0][j]时,总价值都为0.
  2. w[i]>j,即要加入到的物品重量大于背包的总重量时,直接使用上一个单元格的策略,因为这个物品i装不下,所以物品列表有他没他都一样。
  3. w[i]<=j,即要加入的物品重量 小于等于背包的总重量时,出现了新的决策方式:放入该物品,求出剩余的空间,在表格中寻找与剩余空间相等的背包空间,新决策方式的最大价值为,该物品的价值+剩余空间的最大价值。table[i][j] = max{ table[i-1][j] , v[i] + table[i-1][j-w[i]] }

四、代码

public class KnapsackProblem {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] w = {1, 4, 3};//物品的重量
        int[] val = {1500, 3000, 2000}; //物品的价值 这里val[i] 就是前面讲的v[i]
        int m = 4; //背包的容量
        int n = val.length; //物品的个数



        //创建二维数组,
        //v[i][j] 表示在前i个物品中能够装入容量为j的背包中的最大价值
        int[][] v = new int[n+1][m+1];
        //为了记录放入商品的情况,我们定一个二维数组
        int[][] path = new int[n+1][m+1];

        //初始化第一行和第一列, 这里在本程序中,可以不去处理,因为默认就是0
        for(int i = 0; i < v.length; i++) {
            v[i][0] = 0; //将第一列设置为0
        }
        for(int i=0; i < v[0].length; i++) {
            v[0][i] = 0; //将第一行设置0
        }


        //根据前面得到公式来动态规划处理
        for(int i = 1; i < v.length; i++) { //不处理第一行 i是从1开始的
            for(int j=1; j < v[0].length; j++) {//不处理第一列, j是从1开始的
                //公式
                if(w[i-1]> j) { // 因为我们程序i 是从1开始的,因此原来公式中的 w[i] 修改成 w[i-1]
                    v[i][j]=v[i-1][j];
                } else {
                    //说明:
                    //因为我们的i 从1开始的, 因此公式需要调整成
                    //v[i][j]=Math.max(v[i-1][j], val[i-1]+v[i-1][j-w[i-1]]);
                    //v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
                    //为了记录商品存放到背包的情况,我们不能直接的使用上面的公式,需要使用if-else来体现公式
                    if(v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
                        v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
                        //把当前的情况记录到path
                        path[i][j] = 1;
                    } else {
                        v[i][j] = v[i - 1][j];
                    }

                }
            }
        }

        //输出一下v 看看目前的情况
        for(int i =0; i < v.length;i++) {
            for(int j = 0; j < v[i].length;j++) {
                System.out.print(v[i][j] + " ");
            }
            System.out.println();
        }

        System.out.println("============================");
        //输出最后我们是放入的哪些商品
        //遍历path, 这样输出会把所有的放入情况都得到, 其实我们只需要最后的放入
//        for(int i = 0; i < path.length; i++) {
//            for(int j=0; j < path[i].length; j++) {
//                if(path[i][j] == 1) {
//                    System.out.printf("第%d个商品放入到背包\n", i);
//                }
//            }
//        }

        //动脑筋
        int i = path.length - 1; //行的最大下标
        int j = path[0].length - 1;  //列的最大下标
        while(i > 0 && j > 0 ) { //从path的最后开始找
            if(path[i][j] == 1) {
                System.out.printf("第%d个商品放入到背包\n", i); 
                j -= w[i-1]; //w[i-1]
            }
            i--;
        }

    }

}

五、题目2

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

来源:力扣(LeetCode)53题
链接:https://leetcode-cn.com/problems/maximum-subarray

思路

遍历方式 因为可以产生递推关系, 采用动态规划时, 经常通过此种遍历方式, 如 背包问题, 最大公共子串 , 这里的动态规划解法也是以 先遍历出 以某个节点为结束节点的所有子序列 的思路

对于刚接触动态规划的, 我感觉熟悉第三种遍历方式是需要抓住的核心

因为我们通常的惯性思维是以子序列的开头为基准,先遍历出以 a 为开头的所有子序列,再遍历出以 b 为开头的…但是动态规划为了找到不同子序列之间的递推关系,恰恰是以子序列的结束点为基准的,这点开阔了我们的思路。

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }
}

  目录