动态规划(持续更新中~)

Dynamic Programming(动态规划)

一、定义

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

二、方法论

动态规划的核心是状态及状态转移方程
y = d p ( x 1 , x 2 , x 3 , . . . x n ) y = dp(x1,x2,x3,...x_n) y=dp(x1,x2,x3,...xn)

d p ( x 1 , x 2 , x 3 , . . . , x n ) = k d p ( x 1 1 , x 1 2 , x 1 3 , . . x 1 n ) . . . dp(x_1,x_2,x_3,...,x_n) = kdp(x_11,x_12,x_13,..x_1n)... dp(x1,x2,x3,...,xn)=kdp(x11,x12,x13,..x1n)...

确定状态转移方程需要明确

(1)自变量

确定决定每种状态的变量有几个

例如坐标中的x,y等

(2)因变量的意义

即 dp(x1,x2,x3,…x_n)的含义

(3)状态如何变化

即当前状态与其他状态之间的关系

具体如何实施和把握,我们通过下面的题目来探索

三、题解

在这里借用leetcode上关于动态规划的几个题,谈一些自己的见解。在这里和大家一起share。

按照我自己的理解,我循序渐进得通过几个例子谈一下动态规划

Problem One

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

这是一个简单且经典的动态规划(小学生都会做)问题。

根据上述方法论,

(1)(自变量)
我们可以假定(i,j)代表第i行,第j列

(2)(因变量)
dp[i][j]表示Start到(i,j)的路径数量

注意:(1)(2)的合理选取至关重要,可能影响整个算法的复杂度以及状态转移确定的难度

(3)(状态转移)
dp[i][j]如何由前序状态决定?

​ 分析具体问题发现
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] = dp[i - 1][j] + dp[i][j - 1] dp[i][j]=dp[i1][j]+dp[i][j1]

在此之前,我们还必须明确

初始化状态:

dp[0][0] = 1 dp[0][i] = 1 dp[i][0] = 1

变量细节控制

i - 1 > 0

j - 1 > 0

完成上述步骤,我们可以轻而易举写出这道题的代码

public int uniquePaths(int m, int n) {
    
    
    	//state equation
        int[][] dp = new int[m][n];
    
    	//init
        dp[0][0] = 1;
        for (int i = 1; i < n; i++) {
    
    
            dp[0][i] = dp[0][i - 1];
        }
        for (int j = 1; j < m; j++) {
    
    
            dp[j][0] = dp[j - 1][0];
        }
    
    	//transfer
        for (int i = 1; i < m; i++) {
    
    
            for (int j = 1; j < n; j++) {
    
    
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
    
        return dp[m - 1][n - 1];
}

Problem Two

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

这是Problem One的一个小变形,大家可以自行练习

只需要简单修改状态转移即可。

即对存在(i,j)位置存在障碍物时,修改dp[i][j] = 0

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    
    
    	//state equation
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
    
    	//init
        if(obstacleGrid[0][0] == 1) {
    
          //障碍物
            return 0;
        } else {
    
    
            dp[0][0] = 1;
        }
        for (int i = 1; i < n; i++) {
    
    
            if (obstacleGrid[0][i] == 1) {
    
    	//障碍物
                dp[0][i] = 0;
            } else {
    
    
                dp[0][i] = dp[0][i - 1];
            }
        }
        for (int j = 1; j < m; j++) {
    
    
            if (obstacleGrid[j][0] == 0) {
    
    	//障碍物
                dp[j][0] = dp[j - 1][0];
            } else {
    
    
                dp[j][0] = 0;
            }
        }
    
    	//transfer
        for (int i = 1; i < m; i++) {
    
    
            for (int j = 1; j < n; j++) {
    
    
                if (obstacleGrid[i][j] == 1) {
    
    	//障碍物
                    dp[i][j] = 0;
                } else {
    
    
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];
    }

前面的题目当然都只是小试牛刀,现在加点难度

Problem Three

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

我们依然严格执行我们的方法论

(1)自变量是什么?

不妨令(i,j)为从nums的第i个位置到第j个位置的子序列和

(2)因变量是什么?

子序列和

(3)状态怎么转移?
d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + n u m s [ j ] dp[i][j] = dp[i][j - 1] + nums[j] dp[i][j]=dp[i][j1]+nums[j]
初始化状态

dp[0][0] = nums[0];

dp[i][i] = nums[i]

看到这,so easy!!!!!

但是不妨反思一下,这种做法的复杂度是**O(n^2)**吧

这个方法虽然simple但会不会复杂度太高了呢??

显然如果你在leetcode上使用这个复杂度的代码提交,TLE就会来到你身边

因此,我们需要注意:动态规划最大限度降低复杂的有效方法是

减少自变量数目

思考过后…

发现,其实自变量一个就够了

如果利用逐步求解,以连续数组结束位置i为每一步的解,dp[i]记录了以此位置作为子序列结束位置的最大和。

此时,我们需要的子序列一定是dp中最大的一个。

事情就变得简单了。
d p [ i ] = m a x { d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] } dp[i] = max\{dp[i - 1] + nums[i], nums[i]\} dp[i]=max{ dp[i1]+nums[i],nums[i]}
因此,

public int maxSubArray(int[] nums) {
    
    
    //state euqation
    int[] dp = new int[nums.length];
    
    //init
    dp[0] = nums[0]int maxNum = dp[0];
    
   	//transfer
    for (int i = 1; i < nums.length; i++) {
    
    
        dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
        if (dp[i] > maxNum) {
    
    
            maxNum = dp[i];
        }
    }
    
    return maxNum;
}

挑战才刚刚开始,让我们增加一些状态转移方程的难度来感受一下

Problem Four

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

这是一个相当经典的问题,当然解法也有很多种。无论是中心扩散的方法还是Manacher方法思考的难度和实现的难度都不小。不妨来试试动态规划,来简单得解决这个问题。

考虑 “ababa”这个例子。如果我们已经知道 “bab”是回文,那么“ababa”一定是回文,因为它的左首字母和右尾字母是相同的。
因此,我们给出状态转移转移如下:
d p [ i ] [ j ] = t r u e , 如 果 s i , s i + 1 , . . . , s j 是 回 文 串 dp[i][j] = true, 如果s_i,s_{i+1},...,s_j是回文串 dp[i][j]=true,si,si+1,...,sj
于是:

状态转移方程为
d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ]   a n d   s [ i ] = = s [ j ] dp[i][j] = dp[i + 1][j - 1] \space and\space s[i] == s[j] dp[i][j]=dp[i+1][j1] and s[i]==s[j]

public class Solution {

    public String longestPalindrome(String s) {
        int len = s.length();
        if (len <= 1) {
            return s;
        }
        int longestPalindrome = 1;
        String longestPalindromeStr = s.substring(0, 1);
        boolean[][] dp = new boolean[len][len];
        // abcdedcba
        //   l   r
        // 如果 dp[l, r] = true 那么 dp[l + 1, r - 1] 也一定为 true
        // 关键在这里:[l + 1, r - 1] 一定至少有 2 个元素才有判断的必要
        // 因为如果 [l + 1, r - 1] 只有一个元素,不用判断,一定是回文串
        // 如果 [l + 1, r - 1] 表示的区间为空,不用判断,也一定是回文串
        // [l + 1, r - 1] 一定至少有 2 个元素 等价于 l + 1 < r - 1,即 r - l >  2

        // 写代码的时候这样写:如果 [l + 1, r - 1]  的元素小于等于 1 个,即 r - l <=  2 ,就不用做判断了

        // 因为只有 1 个字符的情况在最开始做了判断
        // 左边界一定要比右边界小,因此右边界从 1 开始
        for (int r = 1; r < len; r++) {
            for (int l = 0; l < r; l++) {
                // 区间应该慢慢放大
                // 状态转移方程:如果头尾字符相等并且中间也是回文
                // 在头尾字符相等的前提下,如果收缩以后不构成区间(最多只有 1 个元素),直接返回 True 即可
                // 否则要继续看收缩以后的区间的回文性
                // 重点理解 or 的短路性质在这里的作用
                if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
                    dp[l][r] = true;
                    if (r - l + 1 > longestPalindrome) {
                        longestPalindrome = r - l + 1;
                        longestPalindromeStr = s.substring(l, r + 1);
                    }
                }
            }
        }
        return longestPalindromeStr;
    }
}

Problem Five

给定一个只包含 '('')' 的字符串,找出最长的包含有效括号的子串的长度。

这个问题第一眼看到,学过DS的人一定会想到栈,的确利用栈结构可以把本题轻而易举解答,并且把复杂度控制在O(n),然而动态规划,也很好用。

学习到这里,相信大家应该需要把握的就剩下状态转移的确定(接下来重点讲解)以及细节的初始化(这个可以由测试样例覆盖性测试等去解决)。

因此,我们从这里开始将步骤简化为三部分:

(1)建模

我们定义一个 dp数组,其中第 i 个元素表示以下标为 i 的字符结尾的最长有效子字符串的长度。

(2)初始化

dp[0] = 0;

(3)状态转移
( 1 )   d p [ i ] = 0 ,   i f   s [ i ] = = ′ ( ′ ( 2 )   d p [ i ] = d p [ i − 2 ] + 2 ,   i f   s [ i ] = = ′ ) ′   a n d   s [ i − 1 ] = ′ ( ′ (1)\space dp[i] = 0, \space if \space s[i] == &#x27;(&#x27;\\(2)\space dp[i] = dp[i - 2] + 2, \space if\space s[i] == &#x27;)&#x27; \space and\space s[i - 1] = &#x27;(&#x27; (1) dp[i]=0, if s[i]==((2) dp[i]=dp[i2]+2, if s[i]==) and s[i1]=(

( 3 )   d p [ i ] = d p [ i − 1 ] + d p [ i − d p [ i − 1 ] − 2 ] + 2 ,   i f   s [ i ] = ′ ) ′   a n d   s [ i − 1 ] = ′ ) ′   a n d   s [ i − d p [ i − 1 ] − 1 ] = ′ ( ′ (3)\space dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2,\space\\ if \space s[i]=&#x27;)&#x27; \space and \space s[i−1]=&#x27;)&#x27;\space and \space s[i−dp[i−1]−1]=&#x27;(&#x27; (3) dp[i]=dp[i1]+dp[idp[i1]2]+2, if s[i]=) and s[i1]=) and s[idp[i1]1]=(

class Solution {
    
    
    public int longestValidParentheses(String s) {
    
    
        if (s.length() == 0) {
    
    
            return 0;
        }
        int[] dp = new int[s.length()];
        int maxLen = 0;
        dp[0] = 0;
        for (int i = 1; i < s.length(); i++) {
    
    
            if (s.charAt(i) == '(') {
    
    
                dp[i] = 0;
            } else if (s.charAt(i) == ')' && s.charAt(i - 1) == '(') {
    
    
                if (i - 2 >= 0) {
    
    
                    dp[i] = dp[i - 2] + 2;
                } else {
    
    
                    dp[i] = 2;
                }
            } else if (s.charAt(i) == ')' && s.charAt(i - 1) == ')') {
    
    
                if (i - dp[i - 1] - 1 >=0 && s.charAt(i - dp[i - 1] - 1) == '(') {
    
    
                    if (i - dp[i - 1] - 2 >= 0) {
    
    
                        dp[i] = dp[i - dp[i - 1] - 2] + dp[i - 1] + 2;
                    } else {
    
    
                        dp[i] = dp[i - 1] + 2;
                    }
                } else {
    
    
                    dp[i] = 0;
                }
            } else {
    
    
                dp[i] = 0;
            }
            if (dp[i] > maxLen) {
    
    
                maxLen = dp[i];
            }
        }
        //System.out.println(maxLen);
        return maxLen;
    }
}

未完待续,更困难的问题即将推出!!!!!

预告:正则匹配与动态规划

猜你喜欢

转载自blog.csdn.net/qq_21515253/article/details/95783546