算法:浅谈算法思想中的动态规划(Dynamic Programming),并附六个动态规划算法题的详细解法

动态规划(Dynamic Programming),简称DP,是算法设计技巧的一种(还有贪婪算法、分治算法、回溯算法、随机化算法等)。其主要思想,就是通过把原问题分解为相对简单的子问题的方式求解复杂问题的算法思想,当然,动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。

动态规划的使用规则:

  1. 最优化原理(最优子结构性质):如果一个问题的最优策略它的子问题的策略也是最优的,则称该问题具有 “最优子结构性质”
  2. 无后效性:当一个问题被划分为多个决策阶段,那么前一个阶段的策略不会受到后一个阶段所做出策略的影响
  3. 子问题的重叠性:这个性质揭露了动态规划的本质,解决冗余问题,重复的子问题我们可以记录下来供后阶段决策时直接使用,从而降低算法复杂度

动态规划的求解步骤:

  1. 描述最优解的模型
  2. 递归地定义最优解,也就是构造动态规划方程,也叫状态转移方程。就是每一步下的子问题的最优解,以及和上一步最优解的关系
  3. 将自顶向下的递归,转化为自底向上地计算最优解。现在逆转为每一步下的子问题的最优解,以及和下一步最优解的关系
  4. 最后根据计算的最优值得出问题的最佳策略

以下是我给出的动态规划的五个经典算法案例,以及算法计算过程(附详细注释):

动态规划经典算法题1:爬楼梯

/**
 * @author LiYang
 * @ClassName ClimbStairs
 * @Description 动态规划:爬楼梯
 * @date 2019/12/10 13:53
 */
public class ClimbStairs {

    /**
     * 动态规划:爬楼梯(自顶向下递归实现)
     * 有N阶楼梯,可以一次爬一步,也可以一次爬两步
     * 请问有多少种爬法
     * @param stair 楼梯阶数
     * @return 爬法数量
     */
    public static int climbStairsRecursive(int stair) {
        //如果只有一阶
        if (stair == 1) {
            
            //就只有一种爬法
            return 1;
        }
        
        //如果有两阶
        if (stair == 2) {
            
            //有两种爬法
            return 2;
        }
        
        //如果有stair阶,则stair阶的爬法,就是stair-1阶和stair-2阶
        //爬法的总和。因为stair阶可以通过stair-1阶再爬一阶,或者
        //stair-2阶再爬两阶达到,也就是stair-1和stair-2爬法总和
        return climbStairsRecursive(stair - 1) + climbStairsRecursive(stair - 2);
    }

    
    /**
     * 动态规划:爬楼梯(自底向上实现)
     * 有N阶楼梯,可以一次爬一步,也可以一次爬两步
     * 请问有多少种爬法
     * @param stair 楼梯阶数
     * @return 爬法数量
     */
    public static int climbStairsForward(int stair) {
        //如果只有一阶
        if (stair == 1) {

            //直接返回一种爬法
            return 1;
        }

        //如果有两阶
        if (stair == 2) {

            //直接返回两种爬法
            return 2;
        }

        //答案记录的数组
        //也就是如果有N阶,则爬法数就是solution[N-1]
        int[] solution = new int[stair];
        
        //如果只有一阶,就只有一种爬法
        solution[0] = 1;
        
        //如果有两阶,就有两种爬法
        solution[1] = 2;

        //依次计算3-N阶的爬法
        for (int i = 2; i < solution.length; i++) {
            
            //第i阶的爬法,就是i-1阶和i-2阶的爬法的总和
            //因为第i阶,可以通过i-1阶爬一步和i-2阶爬两步达到
            solution[i] = solution[i-1] + solution[i-2];
        }
        
        //返回stair阶最终的爬法,也就是答案数组最后一个
        return solution[stair - 1];
    }

    
    /**
     * 验证爬楼梯算法
     * @param args
     */
    public static void main(String[] args) {
        //爬楼梯(自顶向下递归实现):climbStairsRecursive(12): 233
        System.out.println("climbStairsRecursive(12): " + climbStairsRecursive(12));
        
        //爬楼梯(自底向上实现):climbStairsForward(12): 233
        System.out.println("climbStairsForward(12): " + climbStairsForward(12));
    }
    
}

动态规划经典算法题2:打家劫舍

/**
 * @author LiYang
 * @ClassName Rob
 * @Description 动态规划:打家劫舍
 * @date 2019/12/10 14:15
 */
public class Rob {

    /**
     * 题目描述:
     * 
     * 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,
     * 影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
     * 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
     * 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动
     * 警报装置的情况下,能够偷窃到的最高金额。
     * 
     * 入参:[2,7,9,3,1]
     * 答案:12
     * 解答:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 
     * (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。
     */

    /**
     * 动态规划:打家劫舍(自顶向下递归实现)
     * @param money 相连房屋金额的数组
     * @param index 当下准备打劫房屋的序号
     * @return 当下能打劫到的最大钱数
     */
    public static int robRecursive(int[] money, int index) {
        //如果是第一间房屋,就下手
        if (index == 0) {
            
            //返回第一间房屋的钱
            return money[0];
        }
        
        //如果是第二间房屋
        if (index == 1) {
            
            //返回前两间房屋中较多的钱,因为相连会报警
            //所以二者只能择其一
            return Math.max(money[0], money[1]);
        }
        
        //如果是index间房屋,则有两种做法:下手或不下手
        //如果下手,则总钱数就是当前房屋的钱数money[index]加上前index-2房间的最大钱数
        //如果不下手,则总钱数就是前index-1房屋的最大钱数
        //我们返回二者中较大的值,作为前index房间的最大钱数
        return Math.max((money[index] + robRecursive(money, index - 2)), 
                robRecursive(money, index - 1));
    }

    /**
     * 动态规划:打家劫舍(驱动程序)
     * @param money 相连房屋金额的数组
     * @return 能打劫到的最大钱数
     */
    public static int robRecursive(int[] money) {
        //调用递归方法,求出最大的钱数
        return robRecursive(money, money.length - 1);
    }


    /**
     * 动态规划:打家劫舍(自底向上实现)
     * @param money 相连房屋金额的数组
     * @return 能打劫到的最大钱数
     */
    public static int robForward(int[] money) {
        //如果是第一间房屋,就下手
        if (money.length == 1) {

            //返回第一间房屋的钱
            return money[0];
        }

        //如果是第二间房屋
        if (money.length == 2) {

            //返回前两间房屋中较多的钱,因为相连会报警
            //所以二者只能择其一
            return Math.max(money[0], money[1]);
        }

        //答案记录的数组
        //solution[i-1],就是记录前i个房间可偷到的最大钱数
        int[] solution = new int[money.length];

        //如果是第一间房屋,就下手,获得第一间房屋的钱
        solution[0] = money[0];
        
        //如果是第二间房屋,就只能在前两间里面取最大值
        //不可能都下手,因为相连会报警,只能二选一
        solution[1] = Math.max(money[0], money[1]);

        //依次计算前第三间到所有房屋的最大钱数
        for (int i = 2; i < money.length; i++) {

            //如果是i间房屋,则有两种做法:下手或不下手
            //如果下手,则总钱数就是当前房屋的钱数money[i]加上前i-2房间的最大钱数
            //如果不下手,则总钱数就是前i-1房屋的最大钱数
            //我们返回二者中较大的值,作为前i房间的最大钱数
            solution[i] = Math.max((money[i] + solution[i-2]), (money[i-1]));
        }

        //返回所有房间的最大钱数,也就是答案数组最后一个
        return solution[money.length - 1];
    }


    /**
     * 验证打家劫舍算法
     * @param args
     */
    public static void main(String[] args) {
        //测试用例:五间房屋,各有一定数目的钱
        int[] money = new int[]{2,7,9,3,1};
        
        //打家劫舍(自顶向下递归实现):robRecursive: 12
        System.out.println("robRecursive: " + robRecursive(money));
        
        //打家劫舍(自底向上实现):robForward: 12
        System.out.println("robForward: " + robForward(money));
    }
    
}

动态规划经典算法题3:三角形最长路径

/**
 * @author LiYang
 * @ClassName TriangleMaxPath
 * @Description 动态规划:三角形最长路径
 * @date 2019/12/10 15:08
 */
public class TriangleMaxPath {

    /**
     *         7
     *       3  8
     *     8  1  0
     *   2  7  4  4
     * 4  5  2  6  5
     * 
     * 题目描述:在上面的数字三角形中寻找一条从顶部到底部
     * 的路径,使得路径上所经过的数字之和最大。路径上的每
     * 一步都只能往左下或右下走。只需要求出这个最大和即可,
     * 不必给出具体路径。
     * 
     * 注意,这道题跟迪杰斯特拉(Dijkstra)算法很像(当然,
     * 迪杰斯塔拉算法也是动态规划,大家可以从我之前写的
     * 图论算法博客中找到迪杰斯特拉算法的相关内容)。依次从
     * 上往下算,如果算到还有更大的值,则更新,如果算到
     * 的是相同或更小的值,则不作处理。这道题就不写递归了,
     * 我直接给出自底向上的算法实现
     */

    /**
     * 返回示例三角形二维数组,读者可以
     * 重新自定义下面的三角形二维数组
     * @return 示例三角形二维数组
     */
    private static int[][] triangle() {
        //直接返回示例三角形二维数组
        return new int[][] {
            {7},
            {3, 8},
            {8, 1, 0},
            {2, 7, 4, 4},
            {4, 5, 2, 6, 5}
        };
    }

    /**
     * 返回用于计算示例三角形走到每一个点对应的最大路径值的记录型二维数组
     * @return 对应的用于计算最大路径值的二维数组
     */
    private static int[][] maxRecord(int[][] triangle) {
        //用于计算最大路径值的二维数组
        //注意,这里二维数组第二个长度为0,
        //可以避免现在JVM就分配内存空间,
        //因为之后里面还要重新定义一系列数组
        int[][] maxRecord = new int[triangle.length][0];

        //遍历三角形二维数组
        for (int i = 0; i < triangle.length; i++) {
            
            //根据三角形每层长度,重新定义最大路径二维数组的每一层
            maxRecord[i] = new int[triangle[i].length];
        }
        
        //返回记录型最大路径二维数组
        return maxRecord;
    }

    /**
     * 以二维数组三角形为依据,计算所有点的路径最大值
     * @param triangle 用于计算的三角形二维数组源数据
     * @param maxRecord 返回动态规划后的所有点的路径最大值二维数组
     */
    private static void calculateMaxPathMatrix(int[][] triangle, int[][] maxRecord) {
        //第一层,最大路径和就是本身
        maxRecord[0][0] = triangle[0][0];
        
        //从第二层开始遍历
        for (int i = 1; i < triangle.length; i++) {
            
            //从第二层开始,头尾的最大路径,就是上一层的头尾加上自身
            maxRecord[i][0] = maxRecord[i-1][0] + triangle[i][0];
            maxRecord[i][i] = maxRecord[i-1][i-1] + triangle[i][i];
            
            //中间的值,就是对应上一层左上和右下的较大的值加上自身
            for (int j = 1; j <= i-1; j++) {
                
                //此处Math.max(maxRecord[i-1][j-1], maxRecord[i-1][j]),就是动态规划
                maxRecord[i][j] = Math.max(maxRecord[i-1][j-1], maxRecord[i-1][j]) + triangle[i][j];
            }
        }
    }

    /**
     * 通过计算后的三角形最大路径二维数组,计算整个三角形的最大路径和
     * @param maxRecord 计算后的三角形最大路径二维数组
     * @return 整个三角形的最大路径和 
     */
    public static int calculateMaxPath(int[][] maxRecord) {

        //最大路径值
        int maxPath = Integer.MIN_VALUE;
        
        //遍历最底层的最大路径数组
        for (int i = 0; i < maxRecord.length; i++) {
            
            //动态记录最大值
            maxPath = Math.max(maxRecord[maxRecord.length - 1][i], maxPath);
        }
        
        //返回最终的答案,也就是示例三角形从上到下路径的最大值
        return maxPath;
    }

    /**
     * 运行算法,求出示例三角形从上到下路径的最大值
     * @param args
     */
    public static void main(String[] args) {
        //得到示例三角形的二维数组
        int[][] triangle = triangle();
        
        //初始化记录用的最大路径二维数组
        int[][] maxRecord = maxRecord(triangle);

        //动态规划算法思想求示例三角形各个点的最大路径
        calculateMaxPathMatrix(triangle, maxRecord);
        
        //根据算出的最大路径二维数组,求出示例三角形从上到下路径的最大值
        int maxPath = calculateMaxPath(maxRecord);
        
        //输出结果:示例三角形从上到下路径的最大值:30
        System.out.println("示例三角形从上到下路径的最大值:" + maxPath);
    }

}

动态规划经典算法题4:求子数组的最大和

import java.util.Arrays;

/**
 * @author LiYang
 * @ClassName MaxSubArray
 * @Description 动态规划:求子数组的最大和
 * @date 2019/12/10 16:19
 */
public class MaxSubArray {

    /**
     * 题目描述:给定一个整数数组,找出和最大的子数组,返回其和
     * 例如,[1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1] 最大子数组[3, 5, -1, 2],和为9
     */

    /**
     * 返回示例原数组
     * @return 示例原数组
     */
    public static int[] getOriginArray() {
        //直接返回示例原数组
        return new int[]{1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1};
    }

    /**
     * 动态规划:寻找和最大的子数组的和
     * @param array 源数组
     * @return 源数组和最大的子数组的和
     */
    public static int maxSumSubArray(int[] array) {
        //第一个元素,子数组和就是本身
        int nStart = array[0];
        
        //第一个元素,最大子数组和也是本身
        int nAll = array[0];
        
        //从数组第二个开始往后遍历
        for (int i = 1; i < array.length; i++) {
            
            //在array[i]的时候,如果前面的子数组和是正数
            if (nStart > 0) {

                //加上就有增益,则nStart[i] = nStart[i-1] + array[i]
                nStart = nStart + array[i];

            //如果前面的子数组是负数或者0
            } else {
                
                //加上nStart[i-1]没什么用,就不加了
                //nStart[i] = array[i],重新开始
                nStart = array[i];
            }
            
            //在所有子数组长度中,寻找曾经出现的最大值
            nAll = Math.max(nStart, nAll);
        }
        
        //返回动态规划的最大子数组之和
        return nAll;
    }

    /**
     * 验证寻找和最大的子数组和的算法
     * @param args
     */
    public static void main(String[] args) {
        //得到示例的数组
        int[] originArray = getOriginArray();

        //根据算法,求得最大和子数组的和
        int maxSum = maxSumSubArray(originArray);

        //输出结果:
        //[1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1]
        //最大和子数组的和是:9
        System.out.println(Arrays.toString(originArray) 
                + "\n最大和子数组的和是:" + maxSum);
    }

}

动态规划经典算法题5:最长公共子串长度问题

/**
 * @author LiYang
 * @ClassName MaxEqualsSubString
 * @Description 动态规划:最长公共子串长度问题
 * @date 2019/12/11 11:37
 */
public class MaxEqualsSubString {

    /**
     * 题目描述:给定两个字符串query和text,均由小写字母组成。要求在text
     * 中找出以同样的顺序连续出现在query中的最长连续字母序列的长度。
     * 例如,query为"acbac”,text为"acaccbabb”,那么text中的"cba”
     * 为最长的连续出现在query中的字母序列,因此,返回结果应该为其长度3
     */

    /**
     * 动态规划:求最长公共子串长度
     * @param text 字符串text
     * @param query 字符串query
     * @return text和query的最长公共子串的长度
     */
    public static int maxEqualsSubStringLength(String text, String query) {
        //先获取text和query的字符数组
        char[] textChars = text.toCharArray();
        char[] queryChars = query.toCharArray();
        
        //初始化记录二维数组,记录从两个字符串各自的某字符开始,
        //可以形成的最长的子串的长度(动态规划)
        int[][] maxRecord = new int[textChars.length][queryChars.length];
        
        //双重遍历,遍历两个字符串的起点字符的所有组合
        for (int iText = 0; iText < textChars.length; iText++) {
            for (int iQuery = 0; iQuery < queryChars.length; iQuery++) {
                
                //如果两个字符串在某一字符上是相等的
                if (textChars[iText] == queryChars[iQuery]) {
                    
                    //这个相同字符的maxRecord的长度自加1
                    maxRecord[iText][iQuery] ++;
                    
                    //获得两个字符共同的上一个字符的下标
                    int pText = iText - 1;
                    int pQuery = iQuery - 1;
                    
                    //如果相等的两个字符之前都还有字符,且曾经有被记录为相等
                    while (pText >= 0 && pQuery >= 0 && maxRecord[pText][pQuery] > 0) {
                        
                        //之前的字符累加1,然后下标再往前移,直到达到
                        //一方的开始字符,或者字符不一样的地方,所至
                        //之处,都累加1,这里也就是动态规划的地方!
                        maxRecord[pText --][pQuery --] ++;
                    }
                }
            }
        }

        //最长公共子串的长度
        int maxLength = 0;
        
        //双重遍历,寻找最长公共子串的长度
        for (int iText = 0; iText < textChars.length; iText++) {
            for (int iQuery = 0; iQuery < queryChars.length; iQuery++) {
                
                //动态更新最长公共子串的长度
                maxLength = Math.max(maxRecord[iText][iQuery], maxLength);
            }
        }
        
        //返回求出的text和query的最长公共子串的长度
        return maxLength;
    }

    /**
     * 验证寻找最长公共子串长度的算法
     * @param args
     */
    public static void main(String[] args) {
        //按照示例,定义text和query
        String text = "acaccbabb";
        String query = "acbac";
        
        //根据算法,求得text和query的最长公共子串长度
        int maxLength = maxEqualsSubStringLength(text, query);
        
        //输出结果:maxLength: 3
        System.out.println("maxLength: " + maxLength);
    }

}

动态规划经典算法题6:最长上升子序列长度问题

import java.util.Arrays;

/**
 * @author LiYang
 * @ClassName LIS
 * @Description 最长上升子序列的长度
 * @date 2019/12/16 15:28
 */
public class LIS {

    /**
     * 本题旨在求解一个数组里面的最长上升子序列的长度,不需要连续
     * 如果有多个子序列长度相同且最长,则返回共同长度
     * 比如:{5, 2, 8, 6, 3, 6, 9, 7}
     * 最长上升子序列为{2, 3, 6, 7} 和 {2, 3, 6, 9},长度都是4
     * 则返回最长上升子序列长度4即可
     */

    /**
     * 求指定数组的最长增长子序列长度
     * @param nums 入参的数组
     * @return nums最长增长子序列长度
     */
    public static int longestIncreaseSubsequenceLength(int[] nums) {
        //如果数组长度为0/1,直接返回其长度0/1
        if (nums.length < 2) {
            return nums.length;
        }
        
        //用于动态计算子序列长度的数组
        int[] count = new int[nums.length];
        
        //初始化所有的子序列长度为1
        Arrays.fill(count, 1);
        
        //最长增长子序列长度,初始化为最小值1
        int maxLength = 1;
        
        //外层循环:数组从右到左,倒数第二个开始,直到数组的开始。
        //分别求以当前元素开始到结束的子数组的最大增长序列长度,
        //往前面走(i--),就把数组元素与后面的比较,如果数组元素小于
        //后面的元素,则该元素的加入可以扩展子序列,然后就在后面
        //找到最大的子序列长度,然后+1,就是当前元素开始的子数组的
        //最大增长子序列长度,直到第一个元素。然后取这里面最大的
        //子序列长度,也就求出了最长增长子序列长度
        for (int i = nums.length - 2; i >= 0; i--) {
            
            //内层循环:从外层循环的下标的下一位开始
            //直到数组的末尾
            for (int j = i + 1; j < nums.length; j++) {
                
                //如果后面的数字大于当前的数字
                if (nums[j] > nums[i]) {
                    
                    //当前元素开始的子数组长度动态更新
                    count[i] = Math.max(count[i], 1 + count[j]);
                }
            }
            
            //动态更新最长增长子序列长度
            maxLength = Math.max(maxLength, count[i]);
        }

        //返回最长增长子序列长度
        return maxLength;
    }

    /**
     * 验证最长上升子序列的长度算法
     * @param args
     */
    public static void main(String[] args) {
        //测试数组
        int[] nums = {5, 2, 8, 6, 3, 6, 9, 7};
        
        //打印最长上升子序列的长度:4
        System.out.println(longestIncreaseSubsequenceLength(nums));
    }
    
}

好了,六个经典的动态规划案例和详解已给出,大家好好研究一下吧

发布了67 篇原创文章 · 获赞 14 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章