初识动态规划

动态规划的英文名为Dynamic Programming,是一种分阶段求解决策问题的数学思想,后来沿用到编程领域。

在运筹学中,动态规划的原理也叫最优化原理,其包括如下性质:
对于多阶段问题的最优化策略,如果用它的前i步策略产生的情况(加上原有的约束条件)来形成一个前i步问题,那么所给最优策略的前i阶段的策略构成这前i步问题的一个最优策略。
在运筹学中用动态规划方法求解多阶段决策问题的一般步骤为:
第1步:明确问题找出阶段数;
第2步:确定变量,找出状态变量和决策变递推关系式量;
第3步:找出状态转移方程;
第4步:写出递推关系式;
第5步:求解递推关系式。
在这些步骤中关键是找出状态转移方程并写出递推关系式,不同的问题递推关系式不同,求解递推关系式的方法也不同,所以动态规划只是提供了求解多阶段决策问题的思路,具体的算法要根据问题的特点去设计。

沿用到编程领域,其思想与数学中也大同小异。
在编程中动态规划的大致思路是把一个复杂的问题转化成一个多阶段逐步递推的过程,从简单的初始状态一步一步递推,最终得到复杂问题的最优解。
在编程中用动态规划解决问题的过程分为两步:
第1步:寻找状态转移方程;
第2步:利用状态转移方程自底向上求解问题
其实很多时候,动态规划不仅仅是一种解题思路还是一种算法的优化策略。
下面举几个例题来演示动态规划的解题思路
斐波那契数列:该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
思路一:(递归) 由题目描述不难得出一个状态转移方程—F(N) = F(N - 1) + F(N - 2),由此马上想到用递归的方法来求解。

private static int fib1(int n) {
        if (n == 1) {
            return 0;
        }
        if (n == 2) {
            return 1;
        }
        return fib1(n - 1) + fib1(n - 2);
    }

此代码虽然可读性高容易理解,但是其时间复杂度高达O(2*n),所以得需要进行优化。
思路二:(动规) 既然已知状态转移方程,而剩下的只需要创建一个储存中间数据的临时数组dp,然后通过循环的手段自底向上,从而用动态规划解决了此题。

private static int fib2(int n) {
        int[] dp = new int[n];
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        dp[1] = 1;
        for (int i = 2; i < n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n - 1];
    }

此方法的时间复杂度降到了线性O(n),其空间复杂度也是O(n)。

最短路径和: 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。(每次只能向下或者向右移动一步)
思路一:(递归) 由最优化原理可知,要想使得当前这一步的路径和最小,就必须使得上一步的路径和的最小值来加上当前路径。即其递推式为—
calculate(grid, i + 1, j)=grid[i][j] + min(calculate(grid, i + 1, j), calculate(grid, i, j + 1))。

private int calculate(int[][] grid, int i, int j) {
        if (i == grid.length || j == grid[0].length) {
            return Integer.MAX_VALUE;
        }
        if (i == grid.length - 1 && j == grid[0].length - 1) {
            return grid[i][j];
        }
        return grid[i][j] + Math.min(calculate(grid, i + 1, j), calculate(grid, i, j + 1));
    }

    public int minPathSum(int[][] grid) {
        return calculate(grid, 0, 0);
    }

和上面的问题一样,此方法的时间复杂度太高,所以需要进行优化。
思路二:(动规) 在这里我们需要构建一个二维数组来储存矩阵当前位置的最短路径,其中此二维数组的长宽都需比原矩阵的长宽大一(方便进行边界处理),由上述递归的递推式不难得出此状态转移方程为–
dp[i + 1][j + 1] = grid[i][j] + min(dp[i][j + 1], dp[i + 1][j])。然后通过双重循环自底向上便可解决此题。

public int minPathSum(int[][] grid) {
        if (grid.length == 0) {
            return 0;
        }
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m + 1][n + 1];//扩边
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0) {
                    dp[i + 1][j + 1] = grid[i][j] + dp[i + 1][j];//边界处理
                } else if (j == 0) {
                    dp[i + 1][j + 1] = grid[i][j] + dp[i][j + 1];//边界处理
                } else {
                    //状态转移方程方程
                    dp[i + 1][j + 1] = grid[i][j] + Math.min(dp[i][j + 1], dp[i + 1][j]);
                }
            }
        }
        return dp[m][n];
    }

最大自序和: 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:(动规) 首先创建一个一维数组来保存当前位置的最大和,由最优化原理得,要想当前位置的和最大,那么就需要判断上一个位置的最大和是否大于0,只要其大于0,那么当前位置的最大和就等于上一个位置的最大和加上当前位置的值,反之直接取当前位置的值。即其状态转移方程为—dp[i] = max(dp[i-1] + nums[i], nums[i]),同理下一步自底向上遍历数组元素即可得解。

public int maxSubArray(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int max = dp[0];//记录其最大的连续最大和
        for (int i = 1; i < nums.length; i++) {
            dp[i] = dp[i - 1] > 0 ? dp[i - 1] + nums[i] : nums[i];
            max = Math.max(max,dp[i]);
        }
        return max;
    }

由于本文都没有给出具体的测试用例和流程分析,读者可以根据思路和代码自己创建测试用例进行分析和理解,正如我最开始所叙述的,动态规划只是一种解题思路,而其中最重要最难的一步就是写出状态转移方程。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值