数据结构与算法——动态规划(1)——动态规划详解

目录

1.基本介绍

1.1 基本思想

1.2 与分治法做比较

1.3 动态规划求解问题

2.经典案例

2.1 0-1背包问题

2.1.1

2.1.2 分析

2.1.3 列表

2.1.4 公式

2.1.5 边界值考虑

2.1.6 代码实现

3.我自己总结的解决动态规划题目的步骤


1.基本介绍

1.1 基本思想

  • 动态规划(Dynamic Programming),又叫表格法
  • 核心思想:
    •  将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
    • 是多阶段决策过程,每步的求解问题是后面阶段求解问题的子问题,用前面问题的结果来做后面问题的及决策,每步决策将依赖于以前步骤的决策结果;
  • 过程:
    • 拆分:将一个复杂问题拆分成一系列的简单子问题,每一次解决一个子问题并将其结果存储起来,理想情况下用基于内存的数据结构
    • 查找:在下一次遇到相同的子问题的时候,直接查找之前计算过的结果而不是重新计算,理想情况下,使用这种方法可以以适当的增大内存占用为代价节省计算时间
    • 记忆化:存储子问题的答案以避免从新计算的技术叫做记忆化

1.2 与分治法做比较

  • 相同点:
    •  与分治方法相似,都是通过组合子问题的解来求解原问题的解 
  • 区别:
    • 分治方法:子问题互不相交,子问题之间是相互独立的 
    • 动态规划:子问题有重叠,即不同的子问题具有公共的子子问题,经分解得到子问题往往不是互相独立的,即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解

1.3 动态规划求解问题

通常求解:

  • 最优化问题 (有很多可行解,每个解都有值,我们希望寻找最小或者最大值)

适用条件:

  • 满足优化原则,即一个最优决策序列的任何子序列本身一定是相对于于子序列的初始和结束状态的最优决策序列

 两个要素:

  • 最优子结构和子问题重叠 
  • 最优子结构
    • 一个问题的最优解包含其子问题的最优解,使用动态规划方法时,我们用子问题的最优解来构造原问题的最优解 

发掘最优子结构的通用模式:

  • 1.证明问题最优解的第一个组成部分是做出一个选择,做出这次选择会产生一个或多个待解的子问题
  • 2.对于一个给定问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择是如何得到的,只是假定已经知道了这种选择。
  • 3.给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间
  • 4.作为原问题最优解的组成部分,每个子问题的就是它本身的最优解

子问题重叠:

  • 导致直接递归的时间复杂度是指数级的,动态规划的方法是付出额外的内存空间来节省计算时间,对每个子问题只求解一次,并将结果保存下来

自底向上法:

  • 将子问题按规模排序,按由小到大的顺序进行求解,当求解某个子问题时,它所依赖的那些更小的问题都已经求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成

实质:

  • 更大问题的最优解要依赖于更小子问题取最优解,子问题取到最优解,子问题的最优解组合到一块就是原问题的最优解

2.经典案例

2.1 0-1背包问题

2.1.1

给定n个物品,一维数组value存储了每个物品的价值,一维数组weight存储了每个物品的重量,再给定一个容量为capacity的背包,向背包中装入物品,我们要在总重量不超过背包容量capacity的情况下,装入背包的总价值最大,且装入物品不能重复

示例1:

数据结构与算法——动态规划(1)——动态规划详解_第1张图片

示例2:

数据结构与算法——动态规划(1)——动态规划详解_第2张图片

2.1.2 分析

  • 比如当我们选择吉他之后,容量变为3,剩下音响和电脑

  • 而接下来就是计算背包容量为3,物品为音响和电脑的总价值最大,这不正是原问题的子问题吗

  • 而且可以发现,其实当我们选择一个物品时,背包容量和物品个数都会发生变化,所以整个变化维度是二维的,即背包容量和物品个数

2.1.3 列表

说明:

  • 第一行表示背包容量,第一列表示物品的个数,对应到物品在上述表中的顺序(程序中为数组的顺序),数字n就表示有前n个物品
  • 其余表示此种容量和物品个数下,装入背包的最大总价值

示例1的列表:

  0 1 2 3 4
0 0 0 0 0 0
1 0 1500 1500 1500 1500
2 0 1500 1500 1500 3000
3 0 1500 1500 2000 3500

 

 

 

 

 

 

所以示例1的最优解为:3500

示例2的列表:

数据结构与算法——动态规划(1)——动态规划详解_第3张图片

所以示例2的最优解为:40

2.1.4 公式

注意:公式的得出完全是按照填表的思路,二维数组的公式很难一下子想到,我们可以填一些表示例,从而根据我们填表时候的思维,分析我们填表的思维步骤,得出对应的公式

说明:

  • 一维数组value[i]表示i物品对应的价值,weight[i]表示i物品对应的重量
  • 二维数组maxValue行i表示物品个数,列j表示背包的容量,

公式:

  • 如果背包容量为0或者物品个数为0时,最大总价值为0,
    • maxValue[0][j]=0;maxValue[i][0]=0;
  • 当增加一个物品的时候,物品的重量大于capacity的时候,最大总价值和只有前一个数量的物品时相同,因为新的物品装不进来,
    • maxValue[i][j] = maxValue[i-1][j];          条件:weight[i]>capacity
  • 当增加一个物品的时候,物品的重量小于capacity的时候,这时我们可以试着装入该物品,装入该物品的话,容量就会减少为j-weight[i],并且当前物品就不能被前面的选择装入,所以i此时装入此物品后的价值最大就是 总价值=该物品的价值+拿了该物品后剩余容量所能容纳的物品最大总价值,即value[i]+maxValue[i-1][j-weight],此时我们要比较装入此物品和不装入此物品的价值(总价值=减掉这个物品获得的最大价值),最终最大的作为当前最大总价值,
    • maxValue[i][j] = max{value[i]+maxValue[i-1][j-weight[i]],maxValue[i-1][j]};          条件:weight[i] <= capacity

2.1.5 边界值考虑

  • 这个问题的边界即为:如果背包容量为0或者物品个数为0时,最大总价值为0,
    • maxValue[0][j]=0;maxValue[i][0]=0;

2.1.6 代码实现

package dynamic.knapsack;

public class KnapsackProblem {

    public static void main(String[] args) {

        //************************示例1*******************
        //物品的重量
        int[] weights01 = {1,4,3};
        //物品的价值
        int[] values01 = {1500,3000,2000};
        //背包的容量
        int capacity01 = 4;

        int example01 = computeMaxValue(values01,weights01,capacity01);

        System.out.println("示例1中的最优解:"+example01);

        //************************示例2*******************
        //物品的重量
        int[] values02 = {1,6,18,22,28};
        //物品的价值
        int[] weights02 = {1,2,5,6,7};
        //背包的容量
        int capacity02 = 11;

        int example02 = computeMaxValue(values02,weights02,capacity02);

        System.out.println("示例2中的最优解:"+example02);
    }

    public static int computeMaxValue(int[] values,int[] weights,int capacity){

        //物品的个数
        int number = values.length;

        /**
         * 创建二维数组
         *
         * maxValue[i][j]表示在i个物品中,能够装入容量为j的背包的最大值
         *
         * 二维数组建立成number+1和capacity+1是因为这样索引最大就使用了maxValue[number][capacity]
         */
        int[][] maxValue = new int[number+1][capacity+1];

        /**
         * 处理边界值
         *
         * 初始化第一行和第一列
         *
         * 因为物品的数量为0时,不管容量为多少,最大总价值都为0
         * 同样,容量为0时,不管物品的数量为多少,最大总价值都为0
         *
         * 也可以不去处理,因为默认为0,这里仅仅为了解决问题步骤的完整性
         */
        for (int i = 0; i < number+1; i++) {
            maxValue[i][0] = 0;
        }

        for (int i = 0; i < capacity+1; i++) {
            maxValue[0][i] = 0;
        }


        /**
         * 根据公式进行填表
         *
         * 下面的i和j从1开始表示不处理第一行和第一列
         */
        for (int i = 1; i < maxValue.length; i++) {
            for (int j = 1; j < maxValue[0].length; j++) {
                //当前物品的重量大于背包的容量j时
                // 下述weights和values的索引为i-1是因为,表中i从1开始,而weights数组种第一个物品的重量从索引0开始
                if(weights[i-1] > j){
                    maxValue[i][j] = maxValue[i-1][j];
                }else {
                    maxValue[i][j] = Math.max(values[i-1]+maxValue[i-1][j-weights[i-1]],maxValue[i-1][j]);
                }
            }
        }

        return maxValue[number][capacity];
    }

}

3.我自己总结的解决动态规划题目的步骤

建议此段总结刷够一定题目再看,仅为自己的思路总结,不一定普适

实质:

  • 求解动态规划问题的实质最重要的一步就是寻找递推公式,
  • 一维线性规划,就是寻找第i个状态(i问题的解)与第i-1或i-2...1之间的任何子问题之间的递推关系
  • 二维线性规划,就是寻找当前i,j状态与更小的i(如i-1)或更小的j(如j-1,背包问题中的j-weight[i])的子问题的解之间的递推关系
  • 由于动态规划都是假设所有子问题的解都是已知的(在实际计算中本身就是已知的,因为我们从下至上计算,然后会记录在表中(对应到程序中就是一维数组或二维数组)),所以当我们找到当前问题与子问题的解的关系时候,就可以通过子问题的解来求当前问题的解

解题步骤:

  • 1.通过找寻题目的性质,找到子问题
  • 2.列表表示一个个子问题递推的过程
  • 3.根据列表中的思维逻辑列出递推公式   (其实这里得出来的公式有点类似于我们在高中学过的递推数列,知道初始值,一步一步根据递推数列计算我们需要的数列值,在动态规划中,其实我们也是知道一些初始值(即下面的边界值),来通过递推公式计算每一步的结果,并且使用表(数组)来记录,从而解决最终问题
  • 4.考虑边界值
  • 5.实现程序

你可能感兴趣的:(数据结构与算法)