算法思想——动态规划

1. 算法思想

    1. 动态规划与分治法相似,都是将先求取子问题的解,然后组合子问题的解得到原问题的解。但是不同之处是:

(1)分治方法是将原问题划分为一个个不相交的子问题(比如前面的归并排序,是将数组不断地划分为一个一个的子数组进行排序,再将返回的两个有序数组进行合并排序)。

(2)动态规划则不同,动态规划解决的是子问题重叠的问题,即不同的子问题会有公共的子子问题,这些重叠的子问题在动态规划中是不应该也不需要重新计算的,而是应该将其解以一定方式保存起来,提供给父问题使用求解。

感觉很抽象,但是下面会详细讲解动态规划的原理,将其与分治思想进行区分,并搞清楚什么情况下要使用动态规划算法思想

动态规划通常用用来求解最优解问题,这类问题会有很多个解,每个解都对应一个值,而我们则是希望在这些解中找到最优解(最大或最小值)。这样的解是问题的一个最优解,因为问题可能不止一个最优解达到了最优值。

    2. 通常四个步骤来设计一个动态规划算法:

(1)刻画一个最优解的结构特征。(这一步是最重要的,必须要刻画出最优解由那些部分构成)

(2)递归的定义最优解的值。

(3)计算最优解的值,通常采用自底向上的方法。

(4)利用计算出的信息构造一个最优解。

    3. 实现方法:动态规划有两种实现方法

(1)自顶向下法:此方法需要一个“备忘录”来辅助实现,备忘录主要用来保存每一个子问题的解(备忘录通常使用散列表或数组结构),当每一个子问题只求解这一次,如果后续需要子问题的解只需查找备忘录中保存的结果,不必重新计算。

(2)自底向上方法:此方法最常用,当此方法使用时,我们必须明确每个子问题规模的概念,使得任何子问题的求解都依赖与子子问题的解来进行求解。

2. 动态规划实例练习

1. 有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。

比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。

再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。

(1)首先分析最优解的结构:

实际上,10级台阶的所有走法可以根据最后一步的不同分为两个部分。

第一部分:最后一步从9级到10级,这种走法的数量和1级到9级的数量一致,也就是Y种。

第二部分:最后一步从8级到10级,这种走法的数量和1级到8级的数量一致,也就是X种。

总的走法就是两种走法的总和,也就是SUM=X+Y种。

F(x) = F(x-1)+F(x-2);而这这公式就是我们最优解的结构,同时也完成了递归定义最优解的值。

F(10) = F(9)+F(8)

F(9)   = F(8)+F(7)

F(8)   = F(7)+F(6)

...

F(3)   = F(2)+F(1)

把一个复杂的问题逐步的简化成简单的问题,通过简单问题的解来求取复杂问题的解,这就是动态规划的思想。

    public static int stepNumber(int number) {
        if (number <= 0) {
            return 0;
        }
        if (number == 1) {
            return 1;
        }
        if (number == 2) {
            return 2;
        }
        return stepNumber(number-1)+stepNumber(number-2);
    }

2. 给定一个包含非负整数的 m x n 阶二维数组,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小,返回最小数字。

说明:每次只能向下或者向右移动一步。

该题的思路是,首先做一个约定,即从arr[0][0]到矩阵中的任意一个arr[i][j]的最小数字总和记录在arr[i][j]中。

那么,arr[m][n]处的数字应为min{arr[m][n]+arr[m-1][n] , arr[m][n]+arr[m][n-1]};

同样的,对于矩阵中任意arr[i][j]的值都应为min{arr[i][j]+arr[i-1][j] , arr[i][j]+arr[i][j-1]},但要注意不能超过矩阵下标,如果某个节点下标是超出矩阵的,就直接按0算;所以该问题的最优解结构的递归定义为:min = min{arr[m][n]+arr[m-1][n] , arr[m][n]+arr[m][n-1]};这样,只需要对整个驻足做一次遍历就能得到结果,时间复杂度为O(m*n),空间复杂度为O(1),代码实现如下

    public static int min(int[][] arr) {
        int m = arr.length;//行
        int n = arr[0].length;//列
        //遍历
        //首先将第一列中到达每一个节点的值进行更新。
        for (int i = 1; i < m; i++) {
            arr[i][0] = arr[i][0] + arr[i-1][0];
        }
        //然后是第一行
        for (int i = 1; i < n; i++) {
            arr[0][i] = arr[0][i] + arr[0][i-1];
        }
        //然后是其他节点上的左边节点与上方节点比较后的最小值相加
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                arr[i][j] = arr[i][j] + Math.min(arr[i-1][j], arr[i][j-1]);
            }
        }
        //遍历结束后得到的就是最小值
        return arr[m-1][n-1];
    }

3. 动态规划原理

对与动态规划,我经常就是看着上面两道题的解答,“哦,一下就懂了”,换一道题,这咋做?所以,我们必须要搞清楚动态规划在什么时候用?以及怎么用?

    1. 最优子结构:动态规划方法求解最优化问题时的第一步就是描述最优解的结构。如果一个问题的最优解是由子问题的最优解得到的,那么就可以说该问题具有最优子结构性质,也就是动态规划解决的问题必须具有最优子结构性质。寻找最优子结构的通用模式为

(1)证明问题最优解的第一个组成部分是做出一个选择,比如例题中选择一步跨2阶还是1阶,每做出一个决策都会产生一个或多个待解决的子问题。

(2)对于一个给定问题,在其可能的第一步选择中,你假定已经知道了第一步中所有可能选择中的最优解,只是假定。

(3)给定最优解的选择后,你确定此次选择会产生那些子问题,以及如何最好的刻画子问题空间。

(4)作为构成原问题最优解的组成部分,每个子问题的解就是它本身最优解。

    2. 重叠子问题:适合动态规划方法求解的最优化问题应具有的第二个性质就是子问题空间必须足够小,即问题的递归算法会反复的求解相同的子问题,而不是一直生成新的子问题,就比如在例题1中,F(3)的最优解来自于F(2)和F(1)之和,也就是说要求出F(2)和F(1)的最优解,而子问题F(2)和F(1)都是已经得到了最优解。而与之相对的就是,分治算法每一步都会产生全新的子问题。

    3. 也就是说,如果我们能够给一个最优解问题找到其第一层子问题的最优子结构,也就是问题的解,同时该子结构也能适用于该问题下更小子问题的最优解,就可以使用动态规划算法。

总之,一定要找到最优解问题的那个“公式”,也就是最优子结构,比如例题中的“F(x) = F(x-1)+F(x-2)”,该公式一定要能刻画出问题及其子问题的最优解,必须要保证父问题的最优解是来由子问题的最优解组合得到,记录下子问题的最优解,在后续对其父问题的求解过程就可以直接使用这些解。只要会找到这个公式就会动态规划算法思想。

 

转载于:https://my.oschina.net/ProgramerLife/blog/3082852

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