Leetcode动态规划部分典型题目分类及总结

2023-11-01

参考内容

 https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/

https://leetcode-cn.com/problems/fibonacci-number/solution/dong-tai-gui-hua-tao-lu-xiang-jie-by-labuladong/

https://leetcode-cn.com/circle/article/NfHhXD/

https://leetcode-cn.com/circle/article/2Xxlw3/

非常感谢上述内容提供者无私的分享

目录

参考内容

动态规划做题一般步骤

1、状态(dp)

2、状态转移方程

3、初始化

4、输出

1. 数塔

2. 背包

3.路径

4骰子

5约束

6区间 DP

7最长子序列/子字符串

8回文子序列/子字符串

9概率

10树状DP


 

动态规划做题一般步骤

1、状态(dp)

状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。

对状态的定义,在不同类型的题目中,各不相同,有些题目dp维数低,有些题目中使用高维数dp

动态规划是非常灵活的一种思维模式,很难像其他算法一样找到通用的模板,在同一类型题目中,dp的设置比较相似,如果再次遇到相似的问题,可以进行参考。

2、状态转移方程

状态转移方程是非常重要的,是动态规划的核心,也是难点;

常见的推导技巧是:分类讨论。即:对状态空间进行分类;

归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;

除了掌握经典的动态规划问题以外,还需要多做题;

如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;

「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。

此处liweiwei1419提及到的填表,是初学者理解动态规划运算过程非常好的一种方式,对于比较难的题目,不仅难在dp方程的定义,也难在初始化和动态规划整个过程多个循环嵌套时,遍历的方向,内外循环彼此位置的安排,有时将内循环变成外循环,很多问题就迎刃而解,有时将正序变成倒数,题目就会得到正确答案,

比如01背包和完全背包,01背包必须倒序进行,完全背包可以正序进行,

同样的,关于股票问题,在base case正确的情况下,将交易次数作为外循环,显然要简单很多,

这并不是一个玄学的过程,通过填表操作,就能很好的理解动态规划的运算过程,也能写出更高质量的代码。

3、初始化

初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。

角度 1:直接从状态的语义出发;

角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;

角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。

填表也是解决初始化问题的方法之一。

4、输出

有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。

动态规划的题目之所以难,是因为题目本身非常考验做题者的抽象思维能力,如何把抽象问题具象化。

以上内容参考liweiwei1419所述,进行了部分的修改和简化

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-

以下内容分类参考leetcode上DP题目分类总结:

https://leetcode-cn.com/circle/article/NfHhXD/

https://leetcode-cn.com/circle/article/2Xxlw3/


1. 数塔

斐波那契数列 到杨辉三角,其中的dp非常好设置,这些数列本身就很有规律性,按着规律就行进行操作,属于动态规划中比较简单和直观的题目

同样,我们会发现,题目要求我们让结果对1e9+7取模,原因大致如下:(来源:知乎@EntropyIncreaser

面试题10- I. 斐波那契数列  https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof/

斐波那契数列的状态方程,初值就是其本身的计算公式,直接进行设置即可

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
class Solution {
public:
    int fib(int n) {
        if(n == 0) return 0;
        if(n == 1) return 1;
        int mod = 1e9+7;
        int F1 = 0,F2 = 1,Fres = 0;
        for(int i = 2;i<=n;++i)
        {
            Fres = (F1 + F2)%mod;
            F1 = F2;
            F2 = Fres;
        }
        return Fres;
    }
};

类似问题:

1137. 第 N 个泰波那契数 https://leetcode-cn.com/problems/fibonacci-number/

118. 杨辉三角 https://leetcode-cn.com/problems/pascals-triangle/

119. 杨辉三角 II https://leetcode-cn.com/problems/pascals-triangle-ii/

杨辉三角和斐波那契数列如出一辙,初值也好,状态方程也好,都是题目给定好的,直接计算即可:

 

class Solution {
public:
    vector<int> getRow(int rowIndex) {
        vector<int> dp(rowIndex+1,1);//行数和列数关系
        for(int i = 2;i<=rowIndex;++i)//行数遍历
        {
            for(int j = 2;j<=i;++j)
            {
                dp[j-1] = dp[j-1] + dp[j-2];
            }
        }
        return dp;  
    }
};

(错误版本) 

class Solution {
public:
    vector<int> getRow(int rowIndex) {
        vector<int> dp(rowIndex+1,1);//行数和列数关系
        for(int i = 2;i<=rowIndex;++i)//行数遍历
        {
            for(int j = i;j>1;--j)
            {
                dp[j-1] = dp[j-1] + dp[j-2];
            }
        }
        return dp;  
    }
};

杨辉三角有个很有意思的地方,就是我们需要倒序(可进行内外循环关系) 

二维动态规划的状态方程很好给出

二维的动态规划随意循环,都是没有问题的,但是如果压缩到一维,内循环必须是倒序,从上面的程序输出结果我们也可以看出,从前往后正序计算,i= 3时,计算dp[2]的时候,dp[2] = dp[2](此时为1) + dp[1](此时为3,不是2,因为正序计算的时候将dp[1]更新了);

这就是在不同状态方程下,我们需要注意的地方,有时候要采用不一样的循环策略

279. 完全平方数 https://leetcode-cn.com/problems/perfect-squares/

此题不如说是一道数学题,题目怎么要求咱们怎么设,状态方程如下:

下面我们需要给平方数一个取值上限,当然直接给n也可以,但是我们知道,一个数值一般都不会大于自身一般的平方

当a大于4的时候确实是如此,那么我们就让a/2为平方数的上限,下面的解法是直接开方,当然效果都是一样的,直接开方的循环次数会更小。

class Solution {
public:
    int numSquares(int n) {
        if(n == 0) return 0;
        vector<int> dp(n+1,n);
        dp[0] = 0;//dp[i]表示组成i,需要最少个数的完全平方数
        for(int i = 1;i<=n;++i)
        {
            for(int j = 1;j*j<=i&&j<=sqrt(n);++j)
            {
                dp[i] = min(dp[i],dp[i-j*j]+1);
            }
        }
        return dp[n];

    }
};

 

面试题14- I. 剪绳子 

https://leetcode-cn.com/problems/jian-sheng-zi-lcof/

 

动态规划算法:

class Solution {
public:
    int cuttingRope(int n) {
        if(n<=0) return 0;
        vector<int> dp(n+1,0);//长度学n的绳子,剪m段后,最大乘积
        //本题比较特殊,因为必须要切一刀
        if(n == 1) return 1;
        if(n == 2) return 1;

        dp[1] = 1;
        dp[2] = 1;
        for(int i = 3;i<=n;++i)
        {
            for(int j = 1;j<i;++j)
            {
                dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }
};

 常规的动态规划思维,设dp,找父问题和子问题之间的联系

我们假设dp为:i米长的绳子,切任意刀,最长乘积为dp[i];

长度为i,怎么切乘积最大呢?那么我们试试就知道了,在1的位置切一刀,再乘上dp[i-1],即为j*dp[i-1]

但是我们还要考虑一种情况,就是直切一刀,即j*(i-1)

dp状态方程如下:

参考内容:https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/xiang-jie-bao-li-di-gui-ji-yi-hua-ji-zhu-dong-tai-/

264. 丑数 II 

https://leetcode-cn.com/problems/ugly-number-ii/

第n个丑数,比如第10个丑数12,是怎么算的呢?

1的丑数有几个?2,3,5

2的丑数呢?4,6,10

3的丑数:6,9,15

4的丑数:8,12,20

图源及算法参考:https://leetcode-cn.com/problems/chou-shu-lcof/solution/mian-shi-ti-49-chou-shu-dong-tai-gui-hua-qing-xi-t/

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int>dp(n,0);
        dp[0] = 1;
        int a = 0,b = 0,c = 0;//三个指针,因为是三个因子,分别记录2,3,5对应的数值内容
        for(int i = 1;i<n;++i)
        {
            int temp2 = dp[a]*2,temp3 = dp[b]*3,temp5 = dp[c]*5;
            dp[i] = min(temp2,min(temp3,temp5));
            if(dp[i] == temp2) a++;
            if(dp[i] == temp3) b++;
            if(dp[i] == temp5) c++;
        }
        return dp[n-1];
    }
};

53. 最大子序和

https://leetcode-cn.com/problems/maximum-subarray/

本题很简单,我们利用动态规划,假设以i结尾的连续子数组,和最大

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        int MAX = INT_MIN;
        vector<int>dp(size+1,INT_MIN);//以i结尾,子序列的最大和
        dp[0] = 0;
        for(int i = 1;i<=size;++i)
        {
            dp[i] = max(dp[i-1] + nums[i-1],nums[i-1]);
            MAX = max(MAX,dp[i]);
        }
        return MAX;
    }
};

对本题进行升级:

152. 乘积最大子数组

https://leetcode-cn.com/problems/maximum-product-subarray/

我们关心思维,继续设以i结尾,最长连续子数组最乘积,但是 

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        vector<int>dp(size+1,0);//以i结尾的,最长连续子数组最大乘积
        //base case
        dp[0] = 0;
        dp[1] = nums[0];
        int MAX = dp[1];
        for(int i = 2;i<=size;++i)
        {
            dp[i] = max(nums[i-1],nums[i-1]*dp[i-1]);
            //选择自身,还是选择乘上前者
            MAX = max(MAX,dp[i]);
        }
        return MAX;
    }
};

我们需要改变策略:

算法参考:https://leetcode-cn.com/problems/maximum-product-subarray/solution/dong-tai-gui-hua-li-jie-wu-hou-xiao-xing-by-liweiw/ 

本题是典型的要求解决无后效性的动态规划,我们对dp进行重新的设置:我们必须让正负号和结果产生联系

那么我们分情况讨论问题

  1. nums【i】>0:最大值依旧最大值,最小值依旧最小值
  2. nums【i】<0:那么最大值将会在乘完后变成最小值,最小值会变成最大值
  3. nums【i】=0:二者直接归零

同样,整个过程需要考虑,nums【i】本身会不会代替最大值和最小值

状态方程如下:

完整版本程序如下:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        vector<vector<int>>dp(size+1,vector<int>(2,0));//以i结尾的,最长连续子数组最大乘积
        //base case
        dp[1][0] = nums[0];
        dp[1][1] = nums[0];
        int MAX = dp[1][1];
        for(int i = 2;i<=size;++i)
        {
            if(nums[i-1]>0)//最大值还是最大值
            {
                dp[i][1] = max(nums[i-1],nums[i-1]*dp[i-1][1]);//最大值
                dp[i][0] = min(nums[i-1],nums[i-1]*dp[i-1][0]);//最大值
            }
            else //最大值和最小值进行转换
            {
                dp[i][1] = max(nums[i-1],nums[i-1]*dp[i-1][0]);//最大值
                dp[i][0] = min(nums[i-1],nums[i-1]*dp[i-1][1]);//最大值
            }
            //选择自身,还是选择乘上前者
            MAX = max(MAX,dp[i][1]);
        }
        return MAX;
    }
};

非常好的一道题

 

2. 背包

Leetcode动态规划——01背包问题:https://blog.csdn.net/qq_41605114/article/details/106059876

Leetcode动态规划—完全背包问题:https://blog.csdn.net/qq_41605114/article/details/106086262

 

3.路径

往后的几道路径问题,题目要求都很相似,都是在棋盘格上,要求从某点出发,最终到达某点,求路径总数,最短路径等等

路径问题的状态设置都非常类似,解题思路也大同小异,下面我们来总结一下:

(1)状态定义:一般都是二维dp,一般定义都是dp[i][j]:从起点到坐标为i,j的点,路径总数/最小路径和/出界概率/.....为dp[i][j]

(2)状态方程:从哪儿来?触发什么内容?

从哪儿来?有些题目(62/63)是从目标点[i][j]点的上面和左侧,有些题目则复杂一些(688/935),是以目标点[i][j]点为中心的八个

位置出发,都可以到目标点[i][j],但是这些都不重要,状态方程就和这些能够到达目标点[i][j]的位置有关,有时候是全部加和,有时

候是比大小,这就要看题意了。

(3)初始值

设置按照逻辑和dp table来就好,路径问题的初值都很直观,很好设置主要分为以下部分:

边界处:需要根据题意进行设置,一般都非常明显

边界外:一般都是0,或者无效状态

边界内部:根据题意,有时需要将全部设置为1或者其他内容,路径问题的初值终点还是在边界外边界处

(4)走N步/移动N次

参考688,935,576等题目,旗子的步长一定要作为外循环,为了保证dp table的前一个状态处于完整状态,不至于漏算少算一些情况!

62. 不同路径 https://leetcode-cn.com/problems/unique-paths/

求路径总数

 

class Solution {
public:
    int uniquePaths(int m, int n) {
        if(m < 0||n < 0) return 0;
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
        //设置dp[i][j]为,从起点出发,到该点的路径数量
        for(int i = 0;i<=m;++i) dp[i][1] = 1;
        for(int j = 0;j<=n;++j) dp[1][j] = 1;
        for(int i = 2;i<=m;++i)
        {
            for(int j = 2;j<=n;++j)
            {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m][n];
        
    }
};

精选答案:https://leetcode-cn.com/problems/unique-paths/solution/kan-liao-jue-dui-dong-de-dong-tai-gui-hua-by-stree/

 

从哪儿来? 

本题要求,只能从往下和往右,那么任意一个具有普世意义的点,要得到该点的路径和,那么只需要知道到达该点上面一个点和左侧一个点的路径和即可。

正如状态方程所示。

63. 不同路径 II https://leetcode-cn.com/problems/unique-paths-ii/

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid.size() == 0) return 0;
        int row = obstacleGrid.size();
        int col = obstacleGrid[0].size();
        vector<vector<int>>dp(row+1,vector<int>(col+1,0));
        for(int i = 1;i<=row;++i){
            for(int j = 1;j<=col;++j){
                if(i == 1&&j == 1) {
                    if(obstacleGrid[i-1][j-1] == 0) dp[i][j] = 1;
                    else return 0;//开始就是障碍,一步都走不了
                }
                else {
                    if(obstacleGrid[i-1][j-1] == 0) {
                        if(i == 1) dp[i][j] = dp[i][j-1];//只能从左侧来
                        else if(j == 1) dp[i][j] = dp[i-1][j];//只能从右边来
                        else dp[i][j] = dp[i-1][j] + dp[i][j-1];
                    }
                    else dp[i][j] = 0;
                }
            }
        }
        return dp[row][col];
    }
};

解法二:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid.empty()) return 0;
        int m = obstacleGrid.size(),n = obstacleGrid[0].size();
        if(obstacleGrid[0][0] == 1||obstacleGrid[m-1][n-1]) return 0;
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        //base case
        for(int i = 1;i<=m;++i){
            if(obstacleGrid[i-1][0] == 1) break;//当前列,一旦遇到阻碍,后面全是0
            dp[i][1] = 1;
        }
        for(int j = 1;j<=n;++j){
            if(obstacleGrid[0][j-1] == 1) break;//当前行,一旦遇到阻碍,后面全是0
            dp[1][j] = 1;
        }
        for(int i = 2;i<=m;++i){
            for(int j = 2;j<=n;++j){
                if(obstacleGrid[i-1][j-1] == 1) dp[i][j] = 0;
                else if(obstacleGrid[i-2][j-1] == 1)//如果该点上面是障碍物
                dp[i][j] = dp[i][j-1];
                else if(obstacleGrid[i-1][j-2] == 1)//左侧
                dp[i][j] = dp[i-1][j];
                else 
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m][n];
    }
};

官方解答:https://leetcode-cn.com/problems/unique-paths-ii/solution/bu-tong-lu-jing-ii-by-leetcode/

此题加入了障碍物,那么整个dp的定义思路和状态方程的定义思路还是不变,只是在细节部分作出修改

 

64. 最小路径和 https://leetcode-cn.com/problems/minimum-path-sum/

题目特别指出,从左上角到右下角,告诉了起点和终点,此题和62题没有什么不一样。

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size(),n = grid[0].size();
        if(m == 0||n == 0) return 0;
        //本题规定了起点和终点,起点是左上角,终点是右下角
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        //dp表示以[1][1]为起点,以ij为结尾,最小路径和
        // dp[1][1] = grid[0][0];
        for(int i = 1;i<=m;++i)
        {
            for(int j = 1;j<=n;++j)
            {
                if(i == 1)//第一行
                {dp[i][j] = dp[i][j-1]+grid[i-1][j-1];continue;}
                if(j == 1)//第一列
                {dp[i][j] = dp[i-1][j]+grid[i-1][j-1];continue;}
                dp[i][j] = min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];
            }
        }
        return dp[m][n];
    }
};

解法二:

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        if(grid.empty()) return 0;
        int row = grid.size(),col = grid[0].size();
        vector<vector<int>>dp(row+1,vector<int>(col+1,1000));//第一列只能从上面来,所以外界必须是大值
        dp[1][1] = grid[0][0];
        for(int j = 2;j<=col;++j)//第一行只能从左侧过来
        {
             dp[1][j] = grid[0][j-1] + dp[1][j-1];
        }
        for(int i = 2;i<=row;++i)
        {
            for(int j = 1;j<=col;++j)
            {
                // if(j>1)
                dp[i][j] = min(dp[i-1][j]+grid[i-1][j-1],dp[i][j-1]+grid[i-1][j-1]);
                // cout<<i<<"  "<<j<<"  "<<dp[i][j]<<endl;
            }
        }
        return dp[row][col];
    }
};

 

参考代码:

https://leetcode-cn.com/problems/minimum-path-sum/solution/zui-xiao-lu-jing-he-by-leetcode/

https://leetcode-cn.com/problems/minimum-path-sum/solution/zui-xiao-lu-jing-he-dong-tai-gui-hua-gui-fan-liu-c/

 

如果我们对上面的题目增加难度,不去规定起点和终点,那么我们应该怎么做?

931. 下降路径最小和 https://leetcode-cn.com/problems/minimum-falling-path-sum/

此题一看,和64题非常相似,但是还是有本质上的差别的,64题规定了起点和终点,931并没有在严格意义上要求起点和终点。

本题有一个问题,路径很多,dp最好的定义方式如下:

从哪儿来? 

要到达指定点,出去边界,从三个方向,j,j+1,j-1。加上题目要求最小值,那么直接对比三者之中谁最小即可。

最后再次对比到达最后一行的最小路径中谁最小,完成输出

 

这种情况,在后面买票问题中也会出现,正序倒序都能解决问题。

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& A) {
        int depth = A.size(),size = A[0].size();
        if(depth == 0||size == 0) return 0;
        vector<vector<int>> dp(depth+1,vector<int>(size+1,0));
        dp[1][1] = A[0][0];
        for(int i = 1;i<=depth;++i)
        {
            for(int j = 1;j<=size;++j)
            {
                if(j == 1&&j == size)
                {dp[i][j] = dp[i-1][j]+A[i-1][j-1];continue;}
                if(j == 1)
                {dp[i][j] = min(dp[i-1][j],dp[i-1][j+1])+A[i-1][j-1];continue;}
                if(j == size)
                {dp[i][j] = min(dp[i-1][j-1],dp[i-1][j])+A[i-1][j-1];;continue;}
                dp[i][j] = min(dp[i-1][j],min(dp[i-1][j-1],dp[i-1][j+1]))+A[i-1][j-1];

            }
        }
        int MIN = INT_MAX;
        for(int j = 1;j<=size;++j)
        {
            MIN = min(MIN,dp[depth][j]);
        }
        return MIN;
    }
};

120. 三角形最小路径和 https://leetcode-cn.com/problems/triangle/

如果对931有了深刻的认识,那么120这道题也就没有什么难度了。

状态及状态方程的定义,都是如出一辙,只是边界条件稍有区别而已。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        if(triangle.empty()) return -1;
    
        int endsize = triangle[triangle.size()-1].size();
        vector<int>dp(endsize+1,10000);
        int MAX = INT_MAX;
        dp[0] = 0;dp[1] = triangle[0][0];
        for(int i = 2;i<=triangle.size();++i){//外循环从前到后
            for(int j = triangle[i-1].size();j>=1;--j){//内循环注意,因为是一维的,所以要从后往前算
                if(j == 1) dp[j] = dp[j] + triangle[i-1][j-1];
                else dp[j] = min(dp[j],dp[j-1]) + triangle[i-1][j-1];
                if(i == triangle.size()) MAX = min(MAX,dp[j]);
            }
        }
        return MAX == INT_MAX?dp[1]:MAX;
    }
};

 主要参考:

https://leetcode-cn.com/problems/triangle/solution/javadong-tai-gui-hua-si-lu-yi-ji-dai-ma-shi-xian-b/ 

本题使用一维dp务必注意循环的起点和终点

935. 骑士拨号器 https://leetcode-cn.com/problems/knight-dialer/

此题可以说是检验上面几道题目的综合版本,虽然不再是简单的从上或者从右到达指定点,但是本质上是一样的。

本题增加了步数限制,所以需要对dp进行进一步约束,增加步数限制

和其他路径题目一样,只不过增加了步数限制

dp定义如下:

vector<vector<vector<unsigned int>>> dp(4+4,vector<vector<unsigned int>>(3+4,vector<unsigned int>(N+1,0)));
//dp表示ij为起点,摁下了N个数字,方案有多少种,根据题意

至于范围为什么如此设置也是有原因的,和状态方程的设置很有关系。

将拨号键范围内的内容,初始化为1,也就是图中蓝色底纹部分。

如果只摁下一个摁键,那么,那么到达每个数字对应的位置的方案就各为1,最终将所有拨号键范围内数字步数加和即可。

最外侧循环是摁下的数字个数,摁下一个摁键的情况已经包含在初始化中,所有从摁下两次开始。

下面分析一些dp状态方程的推导过程,

如果想到到达数字1,可以从图中绿色标记的各个点出发,那么状态方程就很好理解,要第N次到达(摁下)的数字是a,那么第N-1次到达(摁下)的数字只肯是是图中绿色标记的位置,方案总数或者路径总数就是这些可能性的总和。 

 

当然也可以换个思路,从i,j出发,走k步,摁下的号码个数,也是一种理解状态方程的办法

那么我们第k步走出去,能够到达的位置有八个,再从这些位置作为新的起点,走第k-1步,那么就变成了如下的状态:

dp[i][j][k] =  (dp[i-1][j-2][k-1]+dp[i-1][j+2][k-1]+dp[i-2][j-1][k-1]+dp[i-2][j+1][k-1]+dp[i+1][j-2][k-1]+dp[i+1][j+2][k-1]+dp[i+2][j+1][k-1]+dp[i+2][j-1][k-1])%mod;

 完整代码如下:

class Solution {
public:
    int knightDialer(int N) {
        if(N == 0) return 1;
        if(N == 1) return 10;
        int mod = 1e9+7;
        vector<vector<vector<unsigned int>>>dp(3+4+1,vector<vector<unsigned int>>(4+4+1,vector<unsigned int>(N,0)));
        //di,j,走k步,摁下拨号键的组合
        //base case
        for(int i = 2;i<=4;++i)
        for(int j = 2;j<=4;++j) dp[i][j][0] = 1;
        dp[5][3][0]=1;
        //
        int SUM = 0;
        for(int k = 1;k<=N-1;++k)//走的步数
        {
            for(int i = 2;i<=5;++i)
            {
                for(int j = 2;j<=4;++j)
                {
                    if(i == 5&&(j == 2||j == 4)) continue;
                    dp[i][j][k] =  (dp[i-1][j-2][k-1]+dp[i-1][j+2][k-1]+dp[i-2][j-1][k-1]+dp[i-2][j+1][k-1]+dp[i+1][j-2][k-1]+dp[i+1][j+2][k-1]+dp[i+2][j+1][k-1]+dp[i+2][j-1][k-1])%mod;
                    if(k == N-1) SUM=( SUM + dp[i][j][N-1] )%mod;
                }
            }
        }
        return SUM;

    }
};

下面是同类型的一道题目 

688. “马”在棋盘上的概率 https://leetcode-cn.com/problems/knight-probability-in-chessboard/

国际象棋类型的题目,dp[i][j][k]可以理解为从i,j出发,走k步还在棋盘格上的概率。

也可以理解成走了k步,到达i,j的概率。

class Solution {
public:
    double knightProbability(int N, int K, int r, int c) {
        int Size = N+4;
        vector<vector<vector<double>>>dp(Size,vector<vector<double>>(Size,vector<double>(K+1,0)));
        //从i,j出发,走k步,还在棋盘上的概率
        //base  case
        for(int i = 2;i<=Size-3;++i)
        for(int j = 2;j<=Size-3;++j) dp[i][j][0] = 1;//一步不走,就在棋盘上
        for(int n = 1;n <= K;n++)
        {
            for(int i = 2;i<=Size-3;++i)
            {
                for(int j = 2;j<=Size-3;++j)
                {
                    dp[i][j][n] = (dp[i-1][j-2][n-1]+dp[i-1][j+2][n-1]+dp[i-2][j-1][n-1]+dp[i-2][j+1][n-1]+dp[i+1][j-2][n-1]+dp[i+1][j+2][n-1]+dp[i+2][j+1][n-1]+dp[i+2][j-1][n-1])/8.0;
                }
            }
        }
        return dp[r+2][c+2][K];//务必注意输出,你增加了两行两列,自然此处也要增加
    }
};

以上两道题目特别注意,旗子的步长一定要作为外循环,为了保证dp table的前一个状态处于完整状态,不至于漏算少算一些情况! 

主要参考 

https://leetcode-cn.com/problems/knight-probability-in-chessboard/solution/ma-zai-qi-pan-shang-de-gai-lu-by-leetcode/

https://leetcode-cn.com/problems/knight-probability-in-chessboard/solution/zhuang-tai-ji-de-zai-ci-ying-yong-by-christmas_wan/

 

576. 出界的路径数 https://leetcode-cn.com/problems/out-of-boundary-paths/

https://leetcode-cn.com/problems/out-of-boundary-paths/solution/zhuang-tai-ji-du-shi-zhuang-tai-ji-by-christmas_wa/

此题也是一样的,能计算留着界内路径数,也就能计算走出界限路径数,从哪儿来,上下左右都能来,那么就定义dp为该点上下左右出界路径的总和,base case就是将外界一圈,步数为0的区域,不用走就在界外的这些情况设置为零。

class Solution {
public:
    int findPaths(int m, int n, int N, int begin, int end) {
        int mod = 1e9+7;
        vector<vector<vector<unsigned long long>>>dp(m+2,vector<vector<unsigned long long>>(n+2,vector<unsigned long long>(N+1,0)));
        //初始化base case将范围内的,也就是mxn四周的值全部初始化为1
        //dp[i][j][k]从ij出发,走k步走到外界
        for(int j = 0;j<n+2;++j)
        {
            dp[0][j][0] = 1;//第一行
            dp[m+1][j][0] = 1;//最后一行
        }
        for(int i = 0;i<m+2;++i)
        {
            dp[i][0][0] = 1;//第一列
            dp[i][n+1][0] = 1;//最后一列
        }
        for(int k = 1;k<=N;++k)//步长需要放在外侧循环,因为要考虑到所有组合可能性,也是为了让表的其他部分填写完整
        {
            for(int i = 1;i<=m;++i)
            {
                for(int j = 1;j<=n;++j)
                {
                    dp[i][j][k] = (dp[i-1][j][k-1]+dp[i+1][j][k-1]+dp[i][j-1][k-1]+dp[i][j+1][k-1])%mod;
                }
            }
        }
        int sum = 0;
        for(int k = 1;k<=N;++k)
        sum = (sum+dp[begin+1][end+1][k])%mod;

        return sum;

    }
};

上面的题目都是一本道,一路走到黑,我们来看看迂回型路径题目,走到终点之后,还要走回去

741. 摘樱桃

https://leetcode-cn.com/problems/cherry-pickup/

此类题目,我们该怎么设置dp呢?走一条路我们清楚怎么解决,这么来回折腾,还要去往返,怎么下手?

算法参考:https://leetcode-cn.com/problems/cherry-pickup/solution/dong-tai-gui-hua-xiang-xi-jie-xi-tu-jie-by-newhar/

 

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int N = grid.size();
        vector<vector<int>>dp(N+1,vector<int>(N+1,INT_MIN));
        //从i,j出发到N-1
        //base case
        dp[N-1][N-1] = grid[N-1][N-1];//零位置
        for(int sum = 2*N - 3; sum >= 0; --sum)
        for(int i1 = max(0, sum - N + 1); i1 <= min(N-1,sum); ++i1)
        for(int i2 = i1; i2 <= min(N-1,sum); ++i2)
        {
            int j1 = sum - i1, j2 = sum - i2;
            if(grid[i1][j1] == -1 || grid[i2][j2] == -1) 
                dp[i1][i2] = INT_MIN;
            else
                dp[i1][i2] = grid[i1][j1] + (i1 != i2 || j1 != j2)*grid[i2][j2] + max(
                    max(dp[i1][i2+1], dp[i1+1][i2]), 
                    max(dp[i1+1][i2+1], dp[i1][i2])
                );
        }
        return max(0, dp[0][0]); 

// 作者:newhar
// 链接:https://leetcode-cn.com/problems/cherry-pickup/solution/dong-tai-gui-hua-xiang-xi-jie-xi-tu-jie-by-newhar/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

1463. 摘樱桃 II

https://leetcode-cn.com/problems/cherry-pickup-ii/

 

那么状态方程如下:

 

要达到的点的值,加上能达到此点的所有路径的最值

我们利用下面的代码实现:

                int Val = grid[i-1][j1-1]+grid[i-1][j2-1];
                for(int r1 = 0;r1<3;++r1)
                {
                    for(int r2 = 0;r2<3;++r2)
                    {
                        int lastR1 = j1 + D[r1],lastR2 = j2 + D[r2];
                        if(lastR1<=0||lastR1>col||lastR2<=0||lastR2>col) continue;//越界
                        dp[i][j1][j2] = max(dp[i][j1][j2],Val+dp[i-1][lastR1][lastR2]);
                    }
                }

之后就是base case,出发点就是base case

dp[1][1][col] = grid[0][0]+grid[0][col-1];//出发点(0,0)(0,col-1)

本题还有一个最大的雷区,就是有些点,是不能到达的

我们的行,其实也是每个机器人走的步数,就是一直斜着都,如图中的蓝色路径,也到不了橘黄色的禁区,我们不能算禁区的值

需要跳过,那么对j1来说,不能大于i,那么对于j2来说,必须完全大于col-i,这点非常重要,否则在一些示例中会出错

 那么下面就是完整版本的代码,我们按照行进行遍历,然后防止两个机器人相交,不断遍历列的内容,计算最值。

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        if(grid.empty()) return 0;
        int row = grid.size(),col = grid[0].size();
        int D[3] = {-1,0,1};
        vector<vector<vector<int>>> dp(row+1,vector<vector<int>>(col+1,vector<int>(col+1,0)));
        //两个机器人在第i行,第j1和j2列的时候,取的樱桃数目最多
        //base case
        dp[1][1][col] = grid[0][0]+grid[0][col-1];//出发点(0,0)(0,col-1)
        for(int i = 2;i<=row;++i)//从第二行开始,因为从第一行为起点,只能走到第二行
        for(int j1 = 1;j1<=col;++j1)//从第1列到第col-1列
        {
            if (j1 > i) continue;//剔除不可能的位置!非常重要
            for(int j2 = j1+1;j2<=col;++j2)//从第2列到第col列,避开二者相遇的情况
            {
                //9种组合开始
                if (j2 <= col - i) continue;//剔除不可能的位置!非常重要
                int Val = grid[i-1][j1-1]+grid[i-1][j2-1];
                for(int r1 = 0;r1<3;++r1)
                {
                    for(int r2 = 0;r2<3;++r2)
                    {
                        int lastR1 = j1 + D[r1],lastR2 = j2 + D[r2];
                        if(lastR1<=0||lastR1>col||lastR2<=0||lastR2>col) continue;//越界
                        dp[i][j1][j2] = max(dp[i][j1][j2],Val+dp[i-1][lastR1][lastR2]);
                    }
                }
            }
        }
        //最后一行求最值
        int Res = 0;
        for(int j1 = 1;j1<col;++j1)
        {
            for(int j2 = j1+1;j2<=col;++j2) 
            {Res = max(Res,dp[row][j1][j2]);
            // cout<<dp[2][j1][j2]<<endl;
            }
        }
        return Res;
    }
};

算法参考:https://leetcode-cn.com/problems/cherry-pickup-ii/solution/dong-tai-gui-hua-you-hua-by-over-lord/

 


1220. 统计元音字母序列的数目 https://leetcode-cn.com/problems/count-vowels-permutation/

class Solution {
public:
    int countVowelPermutation(int n) {
        int mod = 1e9+7;
        vector<vector<unsigned long long int>>dp(n+1,vector<unsigned long long int>(6,0));
        //dp含义:字符串长度为n,以i结尾的字符串,有几种组合方式
        for(int i= 1;i<=5;++i)
        dp[1][i] = 1;
        for(int size = 2;size<=n;++size)
        {
            for(int i = 1;i<=5;++i)
            {
                if(i == 1)//a
                dp[size][i] = (dp[size-1][2] + dp[size-1][5] + dp[size-1][3])%mod ;//前面可以是2e和5u和3i
                else if(i == 2)//e
                dp[size][i] = (dp[size-1][1]  + dp[size-1][3])%mod;//前面可以是1a和3i
                else if(i == 3)//i
                dp[size][i] = (dp[size-1][2] + dp[size-1][4])%mod;//前面可以是2e和4o
                else if(i == 5)//u
                dp[size][i] = (dp[size-1][4] + dp[size-1][3])%mod;//前面可以是4o
                else //0
                dp[size][i] = dp[size-1][3];//前面可以是3i
            }
        }
        int sum = 0;
        for(int i = 1;i<=5;++i)
        {
            sum = (sum + dp[n][i])%mod;
        }
        return sum;

    }
};

https://leetcode-cn.com/problems/count-vowels-permutation/solution/dang-wo-men-zai-tan-dong-tai-gui-hua-de-shi-hou-wo/

面试题47. 礼物的最大价值

https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int m = grid.size(),n = grid[0].size();
        vector<vector<int>>dp(m+2,vector<int>(n+2,0));
        for(int i = 1;i<=m;++i)
        {
            for(int j = 1;j<=n;++j)
            {
                if(i == 1&&j == 1) dp[i][j] = grid[0][0];
                dp[i][j] = max(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
            }
        }
        return dp[m][n];
    }
};

 

 

 

4骰子

1223. 掷骰子模拟 https://leetcode-cn.com/problems/dice-roll-simulation/

我们先将dp设为如下情况,并推导出状态方程:

代码也很好写,完整代码如下:

        for(int i = 2;i<=n;++i)//骰子次数
        {
            for(int j = 1;j<=6;++j)//本次投掷出的点数
            {
                for(int k = 1;k<=6;++k)//上次投掷出的点数
                {
                    dp[i][j][x] += dp[i-1][k][x];//本次投掷以j结尾,那么就要加上一次是1~6的全部可能
                }
            }
        }

 现在有约束,某个数字连续出现的次数有了限制,我们和股票问题一样,在状态方程后面增加一个状态量:

下面这个部分不好理解:

假设现在1不能连续出现2次,那算第三次的时候,其实只是排除了三个1同时出现的情况,除去次情况

还有连续一次以1结尾的情况,我们必须要考虑,图中黄色线条的组合我们不考虑,但是蓝色我们必须考虑

所以就有了下面的情况

我们现在要判断,本次点数和上次投掷点数是否相同,如果相同,本次就是第t+1次,t从1开始,小于最大出现次数

本次点数j连续出现次数从2一直到最大出现次数为止,所有情况都要考虑一次,程序如下

                    if(j == k)//本次点数和上次一样
                    {
                        for(int t = 1;t<rollMax[k-1];++t)//此时,是k是j无所谓
                        //循环出现次数,因为大前提是连续出现,所以不能等于rollMax[j]
                            dp[i][j][t+1] = (dp[i][j][t+1] + dp[i-1][k][t])%mod;
                    }

如果本次是第一次,那连续出现次数只能是1,那么就加上上次结尾点数的各个出现次数的组合情况即可

                        for(int t = 1;t<=rollMax[k-1];++t)//此时,必须是k,因为此时循环的目的是上一个点连续出现不同次数的全部清空
                        //循环出现次数,因为上次出现的点数和本次点数不一样
                            dp[i][j][1] = (dp[i][j][1] + dp[i-1][k][t])%mod;
                            //相当于连续第一次出现
                    }

代码完整版本: 

class Solution {
public:
    int dieSimulator(int n, vector<int>& rollMax) {
        if(rollMax.empty()||n == 0) return 0;
        int MAX = 0,mod = 1e9+7;
        for(auto item:rollMax) MAX = max(MAX,item);
        vector<vector<vector<int>>>dp(n+1,vector<vector<int>>(7,vector<int>(MAX+1,0)));
        //投掷n次,本次是j,连续出现次数为k次
        for(int i = 1;i<=n;++i){
            //投掷次数
            for(int j = 1;j<=6;++j){
                //本次点数
                if(i == 1&&rollMax[j-1]) {dp[i][j][1] = 1;continue;}//base case
                for(int l = 1;l<=6;++l){
                    //上次点数
                    if(j == l){
                        //上次和本次一样
                        for(int k = 1;k<=rollMax[l-1]-1;++k) 
                        dp[i][j][k+1] = (dp[i][j][k+1] + dp[i-1][l][k])%mod;
                    }
                    else if(j != l){
                        //不一样,从0开始连续
                        for(int k = 1;k<=rollMax[l-1];++k) 
                        dp[i][j][1] = ( dp[i][j][1] + dp[i-1][l][k])%mod; 
                        //本次点数的组合:上次点数各种出现次数的全部情况的总和,所以加的是l,不是j
                    }
                }
            }
        }
        int sum = 0;
        for(int j = 1;j<=6;++j){
            //不同的点数
            for(int t = 1;t<=rollMax[j-1];++t)//以不同点数结尾,连续出现的次数
            sum = (sum + dp[n][j][t])%mod;

        }
        return sum;

    }
};

 

反之,如果不一样,那么就当连续出现1次处理

本题思路参考:https://leetcode-cn.com/problems/dice-roll-simulation/solution/jin-liang-jian-dan-di-ba-si-lu-jiang-ming-bai-by-m/

1155. 掷骰子的N种方法 

https://leetcode-cn.com/problems/number-of-dice-rolls-with-target-sum/

最初的想法:

class Solution {
public:
    int numRollsToTarget(int d, int f, int target) {
        if(target == 0||(d*f)< target) return 0;
        int mod = 1e9+7;
        vector<vector<int>> dp(d+1,vector<int>(target+1,0));
        //dp[i][j] = i个骰子,凑成target的情况数(一个骰子只能投一次)
        for(int k = 1;k<=f&&k<=target;++k)//base case初始化
        {
            dp[1][k] = 1;
        }
        for(int i = 2;i<=d;++i)//骰子个数
        {
            for(int j = 1;j<=target;++j)//能凑成的金额
            {
                for(int k = 1;k<=f;++k)//骰子面额
                if(j-k>=0)
                dp[i][j] = (dp[i][j]+dp[i-1][j-k])%mod;
            }
        }
        return dp[d][target];
    }
};

也很好理解,第i个骰子的情况,要从第i-1个骰子的情况导出,第i个骰子点数为k,那么第i-1个骰子的点数为j-k即可完成目标。

 

5约束

746. 使用最小花费爬楼梯

https://leetcode-cn.com/problems/min-cost-climbing-stairs/

参考内容: 

https://leetcode-cn.com/problems/min-cost-climbing-stairs/solution/yi-bu-yi-bu-tui-dao-dong-tai-gui-hua-de-duo-chong-/

还是分两种情况,是否要踩上目前的所在的台阶

踩上:dp[i-1] + cost[i]

不踩上:dp[i-2] + cost[i-1]

此题务必注意初值的选择,题意也稍不好理解,写代码的时候也要注意。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        //增加一个第0层和第Top层,根据题意,最终要到达Top层
        //到达每一层的方式有两种,从i-1走一步,从i-2走两步,二者求最小即可
        if(cost.empty()) return 0;
        int size = cost.size();
        vector<int>dp(size+2,0);//到底第i层你能获得的花费
        dp[0] = 0;//起点
        dp[1] = cost[0];//到第一层你就没得选
        for(int i = 2;i<=size;++i)
        {
            dp[i] = min(dp[i-2],dp[i-1])+cost[i-1];
        }
        dp[size+1] = min(dp[size+1-1],dp[size+1-2]);//最后一层,及顶层
        return dp[size+1];
    }
};


6股票问题

本文关于股票问题,主要参考一下内容

股票问题模板如下:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   选择 rest  ,           选择 sell      )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   选择 rest  ,           选择 buy         )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

@liweiwei1419对股票问题进行了极其详尽的分析

@labuladong对股票问题进行了整理,给出了一套完整的模板(链接如下:) 

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/

其中也对初值提出了要求:

dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。

dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。

作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity

状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 下面我们来看题目: 

 

121. 买卖股票的最佳时机 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/

只能交易一次 

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2) return 0;
        vector<vector<int>> dp(size+1,vector<int>(2,0));
        //dp表示在第i天,持有或未持有的状态下,所得到的最大利润
        // for(int i = 1;i<=size;++i) dp[i][] = ;
        dp[0][1] = -prices[0];dp[0][0] = 0;
        for(int i = 1;i<=size;++i)
        {
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
            //今天未持有,表示今天卖了,或者昨天就没有持有
            dp[i][1] = max(dp[i-1][1],0-prices[i-1]);
            //今天持有,表示今天买了了,或者昨天就持有
        }
        return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里

    }
};

本题有两种状态:天数,持有还是未持有,dp状态方程一定是要将所有的状态覆盖的,不然不能称之为状态方程

尽可能多的交易

122. 买卖股票的最佳时机 II https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2) return 0;
        vector<vector<int>> dp(size+1,vector<int>(2,0));
        //dp表示在第i天,持有或未持有的状态下,所得到的最大利润
        dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
        dp[0][0] = 0;//没有持有股票,利润为零
        for(int i = 1;i<=size;++i)
        {
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
            //今天未持有,1表示今天卖了,2昨天就没有持有
            dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
            //本题允许完成无数次交易,买入必须在卖出之前,dp[i-1][0]-prices[i-1]表示今天买了
            //今天持有,2表示今天买了了,1昨天就持有
        }
        return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
    }
};

从初始状态方程定base case

增加约束

309. 最佳买卖股票时机含冷冻期 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/

冷冻期

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2) return 0;
        vector<vector<int>> dp(size+1,vector<int>(2,0));
        //dp表示在第i天,持有或未持有的状态下,所得到的最大利润
        dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
        dp[0][0] = 0;//没有持有股票,利润为零
        dp[1][1] = -prices[0];//第1天持有股票,只能买了
        dp[1][0] = 0;//没有持有股票,利润为零
        for(int i = 2;i<=size;++i)
        {
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
            //今天未持有,1表示今天卖了,2昨天就没有持有
            dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i-1]);
            //本题允许完成无数次交易,买入必须在卖出之前,加上要考虑冷冻期,dp[i-2][0]-prices[i-1]表示今天买了
            //今天持有,2表示今天买了了,1昨天就持有
        }
        return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
    }
};

手续费

714. 买卖股票的最佳时机含手续费 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int size = prices.size();
        if(size < 2) return 0;
        vector<vector<int>> dp(size+1,vector<int>(2,0));
        //dp表示在第i天,持有或未持有的状态下,所得到的最大利润
        dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
        dp[0][0] = 0;//没有持有股票,利润为零
        for(int i = 1;i<=size;++i)
        {
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]-fee);
            //今天未持有,1表示今天卖了,2昨天就没有持有
            dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
            //本题允许完成无数次交易,买入必须在卖出之前,dp[i-1][0]-prices[i-1]表示今天买了
            //今天持有,2表示今天买了了,1昨天就持有
        }
        return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
    }
};

限制交易次数

下面会列出dp table,我们从中就可以看出,外循环是天数还是交易次数,都无关紧要。

从状态方程中我们也能看出,先按k = 1计算一行,还是先按照i=1计算一列,问题都不会受到影响

绿色部分对应的dp状态方程如下:

dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);

灰色部分对应的dp状态方程如下:

dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);

 

123. 买卖股票的最佳时机 III https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/

base case 的情况还是一样的,第0天,未持有利润为0,持有为无效状态

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size(),K = 2;
        if(size < 2) return 0;
        vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(K+1,vector<int>(2,0)));
        //表示在第i天,完成k次交易,目前手中是否持有股票的情况下,最大利润
        for(int k = 1;k<=K;++k)
        {
            dp[0][k][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
            dp[0][k][0] = 0;//没有持有股票,利润为零
        }
        for(int i = 1;i<=size;++i)//天数
        {
            for(int k = 1;k<=K;++k)//交易次数
            {
                dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
                // cout<<"i:"<<i<<"0:"<<dp[i][k][0]<<endl;
                //未持有,昨天就未持有,今天才卖的
                dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
                // cout<<"i:"<<i<<"1:"<<dp[i][k][1]<<endl;
                //持有,昨天就持有,今天才买的
            }
        }
        return dp[size][K][0];
    }
};

其中内外循环可以颠倒:

        for(int k = 1;k<=K;++k)//交易次数
        {
            for(int i = 1;i<=size;++i)//天数
            {
                dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
                // cout<<"i:"<<i<<"0:"<<dp[i][k][0]<<endl;
                //未持有,昨天就未持有,今天才卖的
                dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
                // cout<<"i:"<<i<<"1:"<<dp[i][k][1]<<endl;
                //持有,昨天就持有,今天才买的
            }
        }

 

188. 买卖股票的最佳时机 IV https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/ 

 

此处的初值最后是对天数为0的状态开始设置,比较好理解,要去初始化天数为1的情况,不好理解,如果要初始化天数为1,循环从第二天开始,初始化部分如下:

        dp[1][2][0] = -prices[0];//不可能状态
        dp[1][2][1] = -prices[0];//不可能状态

        dp[1][1][0] = 0;//交易一次但是没有持有
        dp[1][1][1] = -prices[0];//不可能状态

        dp[1][0][0] = 0;//不交易不持有,固然是0
        dp[1][0][1] = -prices[0];//不可能状态
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int size = prices.size();
        if(size < 2) return 0;
        if(k >= prices.size()/2)//加一个保险措施,交易次数大于可交易天数的一半
        {
            int sum = 0;
            for(int i = 1; i < prices.size(); i++)
            {
                int val = prices[i] - prices[i - 1];
                sum += (val > 0? val: 0);
            }
            return sum;
        }
        vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(k+1,vector<int>(2,0)));
        //表示在第i天,完成k次交易,目前手中是否持有股票的情况下,所取得的最大利润
        for(int j = 1;j<=k;++j)
        {
            dp[0][j][1] = -prices[0];//第0天持有股票,不可能,因为后面求的是最大值,所以负无穷
            dp[0][j][0] = 0;//没有持有股票,利润为零
        }
        for(int j = 1;j<=k;++j)//交易次数
        {
            for(int i = 1;i<=size;++i)//天数
            {
                dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
                //未持有,昨天就未持有,今天才卖的
                cout<<"i  :"<<i<<"   "<<dp[i][k][0]<<endl;
                dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]);
                //持有,昨天就持有,今天才买的
                cout<<"i  :"<<i<<"   "<<dp[i][k][1]<<endl;
            }
        }
        return dp[size][k][0];

    }
};

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/zhuang-tai-ya-suo-shi-guan-yu-kshi-fou-dao-xu-yao-/

那么k=2也可以这么写

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size(),K = 2;
        if(size < 2) return 0;
        vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(K+1,vector<int>(2,0)));
        //表示在第i天,完成k次交易,目前手中是否持有股票的情况下,所取得的最大利润
        for(int k = 1;k<=K;++k)
        {
            dp[0][k][1] = -prices[0];//第0天持有股票,不可能,因为后面求的是最大值,所以负无穷
            dp[0][k][0] = 0;//没有持有股票,利润为零
        }
        for(int k = 1;k<=K;++k)//交易次数
        {
            for(int i = 1;i<=size;++i)//天数
            {
                dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
                //未持有,昨天就未持有,今天才卖的
                cout<<"i  :"<<i<<"   "<<dp[i][k][0]<<endl;
                dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
                //持有,昨天就持有,今天才买的
                cout<<"i  :"<<i<<"   "<<dp[i][k][1]<<endl;
            }
        }
        return dp[size][K][0];
    }
};

让交易次数为外循环

 

6区间 DP

都是鬼才解法,不看答案鬼才能知道怎么解

1277. 统计全为 1 的正方形子矩阵

https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/

参考内容:

https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/solution/tong-ji-quan-wei-1-de-zheng-fang-xing-zi-ju-zhen-2/

 

(来源:leetcode官方) 

相当巧妙的方式方法,而此解法,也成为了区间DP题目和核心状态方程

第一行第一列的值就是上面表达式的第一行,如果矩阵的值是0,那么也没的算了

我们再次理解一下状态方程:

无非两种情况,dp[i-1][j-1]最小,或者dp[i-1][j]或者dp[i][j-1]最小:

只要三者不为0,最小边长起码是2。

class Solution {
public:
    int countSquares(vector<vector<int>>& matrix) {
        int m = matrix.size(),n = matrix[0].size();
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        //dp[i][j]以ij为右下角的正方形,个数
        int sum = 0;
        for(int i = 1;i<=m;++i)
        {
            for(int j = 1;j<=n;++j)
            {
                if(i == 1||j == 1)//第一行第一列
                dp[i][j] = matrix[i-1][j-1];
                else if(matrix[i-1][j-1] ==0)
                dp[i][j] = 0;
                else 
                dp[i][j] = min(dp[i-1][j],min(dp[i-1][j-1],dp[i][j-1]))+1;
                sum += dp[i][j];
            }
        }
        return sum;
    }
};

 221. 最大正方形

https://leetcode-cn.com/problems/maximal-square/

参考内容:

https://leetcode-cn.com/problems/maximal-square/solution/li-jie-san-zhe-qu-zui-xiao-1-by-lzhlyle/

 

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if(matrix.empty()||matrix[0].empty()) return 0;
        int row = matrix.size(),col = matrix[0].size();
        vector<vector<int>>dp(row+1,vector<int>(col+1,0));
        // 以i,j为右下角的正方形最大的边长
        int L = 0;//记录最大边长
        for(int i = 1;i<=row;++i)
        {
            for(int j = 1;j<=col;++j)
            {
                if(matrix[i-1][j-1] == '1')//只要当目前点是1的时候,才有意义
                {
                    if(i==1||j==1) 
                    dp[i][j] = matrix[i-1][j-1] - '0';
                    //第一行和第一列,组成正方形边长就是自己本身的值
                    else 
                    {
                        //一旦三个接触的地方,有
                        if(dp[i - 1][j] == 0|| dp[i][j - 1] == 0||dp[i - 1][j - 1] == 0)
                        dp[i][j] = matrix[i-1][j-1] - '0';
                        else 
                        dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }
                }
                L = max(L,dp[i][j]);
            }
        }
        return L*L;
    }
};

以上两个题非常的相似,其实dp定义好了就很好计算这种题目,dp都是以ij为右下角的正方形最大的边长

注意,核心是以ij为正方形的右下角,这是非常关键的一个点,可以说是结题的核心了

96. 不同的二叉搜索树

https://leetcode-cn.com/problems/minimum-score-triangulation-of-polygon/

参考内容:

https://leetcode-cn.com/problems/unique-binary-search-trees/solution/bu-tong-de-er-cha-sou-suo-shu-by-leetcode/

https://leetcode-cn.com/problems/unique-binary-search-trees/solution/96bu-tong-de-er-cha-sou-suo-shu-cduo-chong-jie-fa-/

本题技巧性极强。

class Solution {
public:
    int numTrees(int n) {
        if(n == 0) return 0;
        vector<int> dp(n+1,0);
        //长度为n的序列的不同二叉搜索树个数(和序列内容无关,之和长度有关)
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2;i<=n;++i)//长度为i的序列
        {
            for(int j = 1;j<=i;++j)//长度为n的序列元素
            {
                dp[i] += dp[j-1]*dp[i-j];//j为根时,以j为根,将序列分为两个部分
            }
        }
        return dp[n];
    }
};

5419. 两个子序列的最大点积 https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/

来自于一道周赛的题目:


class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int sz1=nums1.size(),sz2=nums2.size();
        vector<vector<int>> dp(sz1+1,vector<int>(sz2+1,-1e8));
        //初值是-1e8

        for(int i=1;i<=sz1;i++){
            for(int j=1;j<=sz2;j++){
                //1.1
                dp[i][j]=nums1[i-1]*nums2[j-1];
                //1.2
                dp[i][j]=max(dp[i][j],dp[i][j]+dp[i-1][j-1]);
                //2
                dp[i][j]=max(dp[i][j],dp[i][j-1]);
                //3
                dp[i][j]=max(dp[i][j],dp[i-1][j]);
                //4
                dp[i][j]=max(dp[i][j],dp[i-1][j-1]);
            }
        }
        return dp[sz1][sz2];
    }
};

// 作者:smilyt_
// 链接:https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/solution/c-dong-tai-gui-hua-yi-dong-by-smilyt_/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

我们可以这么定义dp:

dp[i][j]:第一个序列从1到i,第二个序列从1到j,选择元素相乘得到的最大点积

那么状态方程是什么呢?

1:只选择第i和第j个:dp[i][j] = nums[i]*nums[j]
2:选择第i和第j个:dp[i][j] = dp[i-1][j-1]+nums[i]*nums[j]
3:不选择第i和第j个:dp[i][j] = dp[i-1][j-1];
因为可以自由组合,那么有:
4:选择第i个,不选择第j个:dp[i][j] = dp[i][j-1];
5:不选择第i个,选择第j个:dp[i][j] = dp[i-1][j];

那么我们综合考虑以上四种情况,选择最值即可。

base case全部是-1e8,因为后面要比大小,需要设置为无效模式

312. 戳气球

https://leetcode-cn.com/problems/burst-balloons/

本题难在dp的定义上,初看此题,根本无法入手,一般都是想到回溯法:

从dp table我们能看出我们遍历的方式,j从底层到顶层,i反之

注意dp base case

本题初见,几乎是很难拿下,逆向思维太难想到,因为戳最后一个气球,对比选第一个戳,容易很多,此处因为是最后一个气球,两侧也就只有i和j了,状态方程很好给。

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        //令dp[start][end]表示从start到end之间的气球戳破之后能得到的最大值
        //头尾增加虚拟结点,都是1
        if(!nums.size()) return 0;
        int NewSize = nums.size() + 2;
        vector<int> Newnums(NewSize);
        Newnums[0] = Newnums[NewSize-1] = 1;//收尾是1
        int index = 1;
        for(auto item:nums) {Newnums[index++] = item;}
        //dp设置
        vector<vector<int>>dp(NewSize,vector<int>(NewSize,0));
        //dp:以i开始,j结束,在这个开区间内,气球被戳破后得到硬币的最大数量
        //戳破气球 i 和气球 j 之间(开区间,不包括 i 和 j)的所有气球,可以获得的最高分数为 x
        // i 应该从下往上
        for (int i = NewSize - 2; i >= 0; i--)
        {
            for (int j = i + 1; j < NewSize; j++)
            {
                // 最后戳破的气球是哪个?
                for (int k = i + 1; k < j; k++)
                {
                    dp[i][j] = max(dp[i][j],dp[i][k] + dp[k][j] + Newnums[i]*Newnums[j]*Newnums[k]);
                }
            }
        }
        return dp[0][NewSize-1];

    }
};

7最长子序列/子字符串

子序列默认不连续,子数组默认连续

718. 最长重复子数组 https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/

本题要求重叠子数组,那么显然,默认是连续的,本题并不难,我们直接看代码: 

class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        if(A.empty()||B.empty()) return 0;
        int size1 = A.size(),size2 = B.size();
        vector<vector<int>>dp(size1+1,vector<int>(size2+1,0));
        //第一个数组的以i结尾,第二个数组以j结尾,最长子数组长度
        // 本题此处指的是连续子数组(子数组默认是连续的)
        int MAX = 0;
        for(int i = 1;i<=size1;++i){
            for(int j = 1;j<=size2;++j){
                if(A[i-1] != B[j-1]) dp[i][j] = 0;
                else if(A[i-1] == B[j-1]){
                    if(dp[i-1][j-1] != 0) dp[i][j] = dp[i-1][j-1] + 1;
                    else dp[i][j] = 1;
                }
                MAX = max(MAX,dp[i][j]);
            }
        }
        return MAX;
    }
};

300. 最长上升子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/

给定一个无序的整数数组,找到其中最长上升子序列的长度。

 

截图来源:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/

这题,暴力能不能算,那是当然,从第一个元素遍历,不断和后面的数值比较,大于,更新指针位置顺便更改能组成的长度。

        for (int i = 0; i < n; ++i) 
        {
            for (int j = 0; j < n; ++j) 
            {
                if (cur < nums[j]) 
                {
                    cur = nums[j];
                    length++;
                }
            }
        }

大致逻辑如上,遇到比cur目前指向的大,增加长度,更新cur指向。

那么我们在这个过程中,怎么标记我们找到的序列长度?是以某元素开头的最长序列为x,还是以某元素结尾的最长序列长度为x

这两种方式我们应该选择哪儿一种?选以某元素结尾的最长序列长度为x。循环的次数最少,以第一个元素开头,要循环n次,以第一个元素结尾,只需要循环1次。

那么整个过程中有没有重复的计算呢?暴力解法当然有,下面我们就利用

状态转移方程

dp表是为了解决重叠子问题

截图来源:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int size = nums.size();
        if(size == 0) return 0;
        vector<int> dp(size,1);//存放以某个元素结尾,能组成最长的序列的长度,默认长度就是1
        int MAX = 0;
        for(int i = 0;i<size;++i)
        {
            for(int j = 0;j<i;++j)//j 的上限是 i,因为是以i结尾,只看i前面的数值
            {
                if(nums[j]<nums[i])//前面的数值一旦小于i本身,那么就可以加入i结尾的序列
                {
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
            MAX = max(MAX,dp[i]);//最终要找最长的,那么找到dp的最大值即可。
        }
        return MAX;
    }
};

139. 单词拆分

https://leetcode-cn.com/problems/word-break/

dp[i]表示,第1到i个子字符串,能不能被拆分成字典中的元素。

给两个指针,i和j,i从1开始,终点为字符串长度,意义为每次给出原始字符串的子字符串,看看能不能被拆解为字典中的元素

j表示所给定子字符串的分割位置,将子字符串分割为s1(0,j-1)和s2(j,i-j)

以leetcode举例子,当j=4的时候,s1(0,j-1) = leet,s2(j,i-j) = code;

使用C++库函数substr(pos,n),返回一个string,包含s中从pos开始的n个字符的拷贝

我们两个循环,外循环控制整个字符串中子字符串的分割情况,内循环控制子字符串内部的分割情况

当s1在能被字典中的元素组成的时候,dp[j] == true

s2也能够被字典中的元素组成的时候,words.find(s.substr(j, i-j)) != words.end()

二者同时存在,说明长度为i的子字符串dp[i] == true。

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        set<string> words;
        for(int i=0; i<wordDict.size(); i++){
            words.insert(wordDict[i]);
        }
        vector<bool> dp(s.length()+1);
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && words.find(s.substr(j, i-j)) != words.end() ) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
};

面试题42. 连续子数组的最大和

https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/

53. 最大子序和 https://leetcode-cn.com/problems/maximum-subarray/

dp设置的好,题就好解

求子序列,因为连续,所以有难度,下面我们看看dp和状态方程

以i结尾,要么a重新开头,要么跟在i-1后面,我们在整个过程中记录最大值即可。

以下为各个dp中保存的子序列的长度

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        vector<int>dp(size+1,0);
        //以i结尾的连续子数组,最大和
        //base case
        int MAX = INT_MIN;//因为有负数,所以需要设置为INT_MIN
        for(int i = 1;i<=size;++i)
        {
            dp[i] = max(nums[i-1],nums[i-1]+dp[i-1]);//比较自己本身,和前者加和,谁更大
            MAX = max(MAX,dp[i]);//记录最大和
        }
        return MAX;
    }
};

面试题46. 把数字翻译成字符串

https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/

 

算法参考:https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/solution/dong-tai-gui-hua-dp-by-z1m/

那么状态方程如下,当i-1和i可以翻译的时候,考虑翻译i-1&i,和单独翻译i的情况

翻译不了的时候就只能考虑翻译i了

本题务必注意base case的情况,还有特殊情况,比如506,只有一种情况,因为06如果算了一次,单独的6再算一次,重复了,所以只能算一次

class Solution {
public:
    int translateNum(int num) {
        string nums = to_string(num);
        int size = nums.size();
        vector<int>dp(size+1,0);//以i结尾,能有几种组合方式
        //base case
        dp[1]=1;
        for(int i = 2;i<=size;i++)
        {
            string temp = nums.substr(i-2,2);//此处是i-2
            
            if(nums[i-2]!='0'&&temp<="25"&&temp>="0") 
            //如果是06,那么就不能组成数字,只能按照6算,所以起点不能是0
            {
                if(i == 2) dp[i] = dp[i-1] + 1; 
                //base case初始位置下,第一个和第二个要是可以组成,那就要加1
                else dp[i] = dp[i-1] + dp[i-2]; 
            }
            else dp[i] = dp[i-1]; 
        }
        return dp[size];
    }
};

 

解法二:

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool>dp(s.size()+1,false);
        //长度为 i 的子串前缀,可以完全被分割成指定字符
        unordered_map<string,int>M;
        for(auto item:wordDict) M[item]++;
        dp[0] = true;
        //长度为j的可以被分割,j到i也可以被分割,那么0-i也可以被分割
        //有:dp[j]&&M.find(s.substr(j,i-j)!=M.end()
        for(int i = 1;i<=s.size();++i)//长度
        {
            for(int j = 0;j<i;++j)//起点
            {
                if(dp[j]&&M.find(s.substr(j,i-j))!=M.end()) 
                {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
};

图源及算法参考:https://leetcode-cn.com/problems/word-break/solution/dong-tai-gui-hua-python-dai-ma-by-liweiwei1419-2/

8回文子序列/子字符串

回文子序列或子串

在动态规划中,回文有一个较为通用的模板:

dp[i][j]以ij做收尾的子串是回文的情况

下面是一道非动态规划,但是也有助于我们理解回文:

回文还有一种处理思路:

 

20. 有效的括号

https://leetcode-cn.com/problems/valid-parentheses/

 

class Solution {
public:
    bool isValid(string s) {
        if(s.empty()) return true;
        stack<char> S;//将相关内容压入栈中
        for(char item:s)
        {
            //遇到右值,全部进行记录
            if(item == '(') S.push('(');
            if(item == '{') S.push('{');
            if(item == '[') S.push('[');
            if(item == ')') 
            {
                //栈为空或者不匹配,直接return false
                if(S.empty()) return false;
                if(S.top()!='(') return false;
                else S.pop();//对比完记着一定要弹出
            }
            if(item == '}') 
            {
                if(S.empty()) return false;
                if(S.top()!='{') return false;
                else S.pop();
            }
            if(item == ']') 
            {
                if(S.empty()) return false;
                if(S.top()!='[') return false;
                else S.pop();
            }
        }
        return S.size()?false:true;//同样的,对比完成后栈必须是空的,不能有遗留
    }
};

在判断回文的时候也要务必注意:子串和子序列是有区别的,子串必须是连续的,子序列可以是离散的 

所以判断子串是不是回文,一般两端,及起点和终点不相等,基本可以判断不是回文

子序列不同,子序列只要保证元素相对的顺序,不需要是连续的

 

5. 最长回文子串 

https://leetcode-cn.com/problems/longest-palindromic-substring/

 https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/(主要参考)

所以,我们设dp如下:

状态方程也很好列出:

本题还有个细节,就是dp table的情况,看下图

所以需要控制后限,如下程序所示,保证都算了

我们再来看看完整版本的dp table

 

如果我们要求1到6子串是不是回文,此时1和6相等,那么剩下的就完全取决于2到5了

所以首先要计算2到5,也就是控制终点,从1到size,起点在1到终点j之间即可。

从图上也很清晰的可以看出,我们应该怎么进行计算

class Solution {
public:
    string longestPalindrome(string s) {
        int size = s.size();
        if(size == 0) return "";
        int MAX = 0;
        string Res;
        vector<vector<bool>>dp(size+1,vector<bool>(size+1,false));
        for(int i = 1;i<=size;++i) dp[i][i] = true;//单个元素算回文
        for(int j = 1;j<=size;++j)//必须固定后限进行循环
        {
            for(int i = 1;i<=j;++i)
            {
                if(s[i-1] == s[j-1])
                {
                    // cout<<"123"<<endl;
                    if(j-i+1<=3) dp[i][j] = true;
                    else dp[i][j] = dp[i+1][j-1];
                }
                else 
                dp[i][j] = false;
                
                if(dp[i][j]&&MAX<(j-i+1))
                {
                    MAX = j-i+1;
                    Res = s.substr(i-1,MAX);
                    // cout<<"MAX  "<<MAX<<endl;
                }
            }
        }
        return Res;
        
    }
};

解法二:计算距离

class Solution {
public:
    string longestPalindrome(string s) {
        if(s.empty()) return "";
        vector<vector<int>>dp(s.size()+1,vector<int>(s.size()+1,0));
        string res;
        int MAX = INT_MIN;
        for(int i = s.size();i>=1;--i){
            for(int j = i;j<=s.size();++j){
                if(i == j) dp[i][j] = 1;
                else if(s[i-1] == s[j-1]){
                    if(j-i<3) dp[i][j] =j-i+1;
                    else if(dp[i+1][j-1] == j-i-1) dp[i][j] = 2+dp[i+1][j-1];
                    else dp[i][j] = dp[i+1][j-1];
                }
                else dp[i][j] = dp[i+1][j-1];
                if(MAX<dp[i][j]){
                    MAX = dp[i][j];
                    res = s.substr(i-1,j-i+1);
                }
            }
        }
        return res;
    }
};

 

647. 回文子串

https://leetcode-cn.com/problems/palindromic-substrings/

本题为上一道题目的扩展,我们增加一个计数功能即可,其他从dp到base case都一样

class Solution {
public:
    int countSubstrings(string s) {
        if(s.empty()) return 0;
        int size = s.size();
        int Res=0;
        vector<vector<bool>>dp(size+1,vector<bool>(size+1,false));
        //base case
        for(int i = 1;i<=size;++i) dp[i][i] = true;
        for(int i = size;i>=1;--i)//起点
        {
            for(int j = i;j<=size;++j)//终点
            {
                if(i == j&&dp[i][j]) {Res++;continue;}
                if(s[i-1] == s[j-1]&&j-i < 2) dp[i][j] = true;
                else if(s[i-1] != s[j-1]) dp[i][j] = false;
                else dp[i][j] = dp[i+1][j-1];

                //统计结果
                if(dp[i][j]) Res++;
            }
        }
        return Res;
    }
};

 

516. 最长回文子序列

https://leetcode-cn.com/problems/longest-palindromic-subsequence/ 

我们把子串改成子序列,难度升级。

就是状态方程有所改变,因为是求子序列的最长长度,求啥设啥,就让dp为长度

我们继续按照上一题的思路思考,此时是子序列,而之前是子串,子串是必须连续的,那么我们只考虑i+1和j-1的情况就可以了

但是子序列内容,需要考虑情况就自然要多

当i和j不相等的时候,我们要考虑整个组合的情况,三者比大小,最大的作为dp[i][j]的值。下面我们列出状态方程:

 

下面我们看看dp table,看看如何计算:

本题还有一个要点,就是当i和j相等的时候,dp[i+1][j-1]+2

这也是关键,举例:bbbab

因为是子序列,完全忽视了子串的连续性,只要相等,起码两个

j要从底端晚上算,i要从小到大算,base case不用计算已经给好了

完整代码如下:

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int size = s.size();
        if(size == 0) return 0;
        vector<vector<int>> dp(size+1,vector<int>(size+1,0));
        //以i开始,j结束,字符串在此区间内最长的回文子序列
        for(int i = 1;i<=size;++i) dp[i][i] = 1;
        for(int j = 1;j<=size;++j)
        {
            for(int i = j-1;i>=1;--i)
            {
                if(s[i-1] == s[j-1]) dp[i][j] = dp[i+1][j-1]+2;
                else
                dp[i][j] = max(dp[i+1][j-1],max(dp[i+1][j],dp[i][j-1]));
            }
            
        }
        return dp[1][size];

    }
};

 

132. 分割回文串 II

https://leetcode-cn.com/problems/palindrome-partitioning-ii/

参考解法:

https://leetcode-cn.com/problems/palindrome-partitioning-ii/solution/dong-tai-gui-hua-by-liweiwei1419-2/

https://leetcode-cn.com/problems/palindrome-partitioning-ii/solution/dong-tai-gui-hua-jie-fa-c-by-bike666222/

分割回文串的第一题是用回溯算法解决的,本题难度升级,我们需要使用动态规划,不同于上面两道题目的设法,那样太复杂了,本题dp这么定义:

我们下面看这么一种情况

base case要谨慎处理:

        //长度为n的字符,最多分割n-1次,以此作为base case
        for(int i = 1;i<=size;++i)
        dp[i] = i-1;

核心代码:

        //本题的核心代码
        for(int i = 1;i<=size;++i)
        {
            if(checkPalindrome[1][i]) {dp[i]=0;continue;}//如果从头到i,都是回文,那就不用分割
            for(int j = 1;j<i;++j)
            {
                //开始进行分割,0到j,j+1到i,检查j+1到i是不是回文,如果是,就切一刀
                if(checkPalindrome[j+1][i]) dp[i]=min(dp[i],dp[j]+1);
            }
        }

 

完整代码:

class Solution {
public:
    int minCut(string s) {
        int size = s.size();
        if(size == 0) return 0;
        vector<int>dp(size+1,0);//从1到i(前i个字符),字符串分割为回文,需要最少的切割次数
        //长度为n的字符,最多分割n-1次,以此作为base case
        for(int i = 1;i<=size;++i)
        dp[i] = i-1;

        //创建二维数组用于记录子串s[a:b]是否为回文串,且一开始全部初始化为false(可以发现a<=b)
        //此部分和5的核心代码是一样的
        vector<vector<bool>> checkPalindrome(size+1, vector<bool>(size+1, false));
        for(int i = 1;i<=size;++i) checkPalindrome[i][i] = true;//单个元素算回文
        for(int j = 1;j<=size;++j)//必须固定后限进行循环
        {
            for(int i = 1;i<=j;++i)
            {
                if(s[i-1] == s[j-1])
                {
                    if(j-i+1<=3) checkPalindrome[i][j] = true;
                    else checkPalindrome[i][j] = checkPalindrome[i+1][j-1];
                }
            }
        }
        //本题的核心代码
        for(int i = 1;i<=size;++i)
        {
            if(checkPalindrome[1][i]) {dp[i]=0;continue;}
            for(int j = 1;j<i;++j)
            {
                //开始进行分割,0到j,j+1到i,检查j+1到i是不是回文,如果是,就切一刀
                if(checkPalindrome[j+1][i]) dp[i]=min(dp[i],dp[j]+1);
            }
        }
        return dp[size];
    }
};

730. 统计不同回文子字符串

https://leetcode-cn.com/problems/count-different-palindromic-subsequences/

https://leetcode-cn.com/problems/count-different-palindromic-subsequences/solution/tong-ji-bu-tong-hui-wen-zi-zi-fu-chuan-by-leetcode/

 

面试题19. 正则表达式匹配

https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/

整体逻辑:

1  正常情况:那就是看二者相等不相等就可以了

2  ‘.’:和上面一样,遇到‘.’就是遇到万能内容

3 ‘*’:此时我们需要分类讨论,*可以取怎么的值?我们后续讨论

 

如果字符相等,那么当然是能匹配了

如果是.,万能字符,也是当然没有问题了

以上两种情况,很容易就写出来了:

if(s[i-1] == p[j-1]||p[j-1] == '.')//相等或者是.,那么就没有问题
dp[i][j] = dp[i-1][j-1];

那么如果是'*',那么该怎么办:

 

就要看*前面的字符

我们来看i和j-1的情况,不匹配,那么直接跳过,也可以认为是*前面的元素出现了0次

if(s[i-1]!=p[j-2] && p[j-2]!='.') dp[i][j]=dp[i][j-2];

那么我们继续讨论剩下的情况,如果二者相等,或者p为万能元素.呢?

那么就要分三种情况来讨论了

*可以让前面的元素出现0,1,无数次,我们就这么分类来比较

第一种情况,*让前面的元素出现0次

第二种情况,*让前面的元素出现一次

第三种情况,*让前面的元素出现多次

三种一种是true就可以

dp[i][j]=dp[i][j-1] || dp[i][j-2] || dp[i-1][j];

算法参考:https://leetcode-cn.com/problems/regular-expression-matching/solution/dong-tai-gui-hua-zen-yao-cong-0kai-shi-si-kao-da-b/

细节1:

        s=" "+s;//防止该案例:"aab" "c*a*b"
        p=" "+p;

一上来就是*,如下:

细节2:不需要保护程序

 if(s.empty()||p.empty()) return false;

 

完整代码:

class Solution {
public:
    bool isMatch(string s, string p) {
        s=" "+s;//防止该案例:"aab" "c*a*b"
        p=" "+p;
        int m=s.size(),n=p.size();
        vector<vector<int>>dp(m+1,vector<int>(n+1,false));
        //dp含义:s中的前i个,和p中的前j个,是否匹配
        //base case
        dp[0][0]=true;
        for(int i = 1;i<=m;++i)
        {
            for(int j = 1;j<=n;++j)
            {
                if(s[i-1] == p[j-1]||p[j-1] == '.')//相等或者是.,那么就没有问题
                dp[i][j] = dp[i-1][j-1];
                else if(p[j-1]=='*')//最难处理的部分
                {
                    if(s[i-1]!=p[j-2] && p[j-2]!='.') dp[i][j]=dp[i][j-2];
                    else dp[i][j]=dp[i][j-1] || dp[i][j-2] || dp[i-1][j];
                }
            }
        }
        return dp[m][n];
    }
};

72. 编辑距离

https://leetcode-cn.com/problems/edit-distance/

 

字符串类型的dp一般设法都显示,我们dp定义如下:

那么有三种情况需要注意:也就是题目中所述的替换,插入,删除

那么我们的状态方程就是求上面三种操作的最小值

下面是base case

如果一个字符串长度为0,那么要将其变成一个非零的字符串,需要的操作数就是非零字符串的长度

完整代码:

class Solution {
public:
    int minDistance(string word1, string word2) {
        int s = word1.size(),p = word2.size();
        if(s == 0&&p == 0) return 0;
        //一个字符串为0,那么操作数就是另外一个字符串的长度
        if(s == 0&&p!=0) return p;
        if(s!=0&&p==0) return s;
        vector<vector<int>>dp(s+1,vector<int>(p+1,0));
        //word1前i个,及word2前j个,匹配需要的最少操作数
        //base case 一个字符串为0,那么操作数就是另外一个字符串的长度
        for(int i = 0;i<=s;++i) dp[i][0] = i;
        for(int j = 1;j<=p;++j) dp[0][j] = j;

        for(int i = 1;i<=s;++i)
        {
            for(int j = 1;j<=p;++j)
            {
                if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];//不操作
                else
                dp[i][j] = 1+min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]));

            }
        }
        return dp[s][p];
    }
};

 

32. 最长有效括号

https://leetcode-cn.com/problems/longest-valid-parentheses/

那么第一种情况很好想到:

那么下面这种情况呢?

 

那么我们不知道之前的情况,因为要给i处的)找到(,我们需要看dp[i-1]的情况,如果是dp[i-1]不为0,如图,那么跨过整个dp[i-1]覆盖的区域,找到i-1-dp[i-1],如果可以和i处的匹配,那么显然是可以组成的

本题最难的地方,也是本题最为巧妙的地方 

class Solution {
public:
    int longestValidParentheses(string s) {
        //动态规划
        if(s.empty()) return 0;
        int size = s.size();
        vector<int>dp(size,0);//以i结尾的最长合法子串长度
        int MAX = 0;
        dp[0] = 0;
        for(int i = 1;i<size;++i)
        {
            if(s[i] == ')') 
            {
                if(s[i-1] == '(')
                {
                    if(i-2>0&&dp[i-2]!=0) dp[i] =  dp[i-2] + 2;
                    else dp[i] = 2;
                }
                else if(i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '(')
                //我们查看dp[i-1]的情况,如果是0,那么s[i-1]就是),不进行任何操作
                //如果dp有值,也就是说以i-1结尾是能组成有效括号的,那么我们去查看
                //索引为i - 1 - dp[i-1]元素的情况,如果是(,那么恰好可以和i的括号组成有效括号
                //最后我们依旧要加上i-1-dp[i-1]-1索引下的dp
                {
                    if(i-1-dp[i-1]-1>0)
                    dp[i] = 2+dp[i-1]+dp[i-1-dp[i-1]-1];
                    else dp[i] = 2+dp[i-1];
                }
            }
            else dp[i] = 0;
            MAX = max(MAX,dp[i]);
        }
        return MAX;
        
    }
};

 

9概率

 

概率问题,要不就设分子,要不就设整体,哪儿个好来用哪儿个

面试题60. n个骰子的点数

https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/

本题难点在于dp的定义

定义dp为:

class Solution {
public:
    vector<double> twoSum(int n) {
        vector<double> Res;
        vector<vector<int>>dp(n+1,vector<int>(6*n+1,0));
        //投掷j个骰子,面数是i出现的次数
        for(int i = 1;i<=n;++i)//投掷次数
        {
            for(int j = i;j<=6*i;++j)//出现点数和,注意起点,2个骰子不可能有1,所以j = i起步
            {
                if(i == 1) {dp[i][j] = 1;continue;}//base case
                for(int val = 1;val<=6;++val)//第n个骰子投出的点数
                {
                    if(j - val >0)//保护程序
                    dp[i][j] += dp[i-1][j-val];
                }
            }
        }
        for(int j = 1;j<=6*n;++j)
        {
            if(dp[n][j]!=0)
            {Res.push_back(dp[n][j]*pow(1.0/6, n));}
        }
        return Res;


    }
};

程序中有很对的细节需要注意。 

写法二:

class Solution {
public:
    vector<double> twoSum(int n) {
        if(n == 0) return {};
        vector<double>Res;//n~6*n之间
        vector<vector<double>>dp(n+1,vector<double>((n+1)*6,0));
        //i个筛子,组成和为j的概率
        //base case
        double g = (1.0/6);
        // cout<<g<<endl;
        for(int j = 1;j<=6;++j) 
        {
            dp[1][j] = g;//初始化
        }
        for(int i = 2;i<=n;++i)//个数
        {
            for(int j = i;j<=i*6;++j)
            {
                for(int l = 1;l<=6;++l)
                {
                    if(j-l>=0) 
                    {
                        dp[i][j] += (g)*dp[i-1][j-l];//第i个显示l,那么前面就要组成j-l才行
                    }
                    else break;
                }
                // cout<<i<<" "<<j<<endl;
            }
        }

        for(int j = n;j<=n*6;++j)
        {
            Res.push_back(dp[n][j]);
        }
        return Res;
    }
};

837. 新21点

https://leetcode-cn.com/problems/new-21-game/

https://leetcode-cn.com/problems/new-21-game/solution/xin-21dian-by-leetcode-solution/

下面是leetcode官网的解答:

假设dp[x]为她手上牌面为x时,能获胜的概率,那么这个概率应该是:
dp[x]=1/w * (dp[x+1]+dp[x+2]+dp[x+3]...+dp[x+w])


来源:https://leetcode-cn.com/problems/new-21-game/solution/huan-you-bi-zhe-geng-jian-dan-de-ti-jie-ma-tian-ge/
 

本题颇有难度,而且属于不常见的从后往前递推的情况,具体讲解和代码请看官网的解答。

10树状DP

198. 打家劫舍

https://leetcode-cn.com/problems/house-robber/

可以参考下文,给出了整个过程的动态流程

https://leetcode-cn.com/problems/house-robber/solution/hua-jie-suan-fa-198-da-jia-jie-she-by-guanpengchn/

本题还是比较简单的,我们这dp如下:

状态方程很好列

偷了,那么第i-1家你就不能碰,不偷,就看i-1家的情况

此题的base case:dp[0] = 0;因为有dp[i-2]所以需要衡量第一家偷不偷,那么如果只有一家,当然是偷了,dp[1] = nums[0]

下面是完整版本的代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        int size = nums.size();
        if(size == 0) return 0;
        vector<int> dp(size+1,0);
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i = 2;i<=size;++i)
        {
            dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
        }
        return dp[size];
    }
};

 

213. 打家劫舍 II https://leetcode-cn.com/problems/house-robber-ii/

最初未优化版本:比较两次,重复两次打家劫舍1,这也是本题最稳妥的解法

class Solution {
public:
    int rob(vector<int>& nums) {
        int size = nums.size();
        if(size == 0) return 0;
        if(size == 1) return nums[0];
        vector<int> dp(size+1,0);
        return max(begin(dp,nums[0],nums),begin(dp,0,nums));//两种情况,第一家偷与不偷
    }
    int begin(vector<int> & dp,int beginval,vector<int> & nums)
    {
        int size = nums.size();
        dp[1] = beginval;
        for(int i = 2;i<=size;++i)
        {
            if(i == size&&dp[1] != 0) dp[i] = dp[i-1];//最后一家只能不偷
            else dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);//正常情况
        }
        return dp[size];
    }
};

337. 打家劫舍 III

https://leetcode-cn.com/problems/house-robber-iii/

本以为是一道BFS的题目,直接开始写:

class Solution {
public:
    int rob(TreeNode* root) {
        //BFS
        if(root == nullptr) return 0;
        queue<TreeNode*>Q;
        Q.push(root);
        int level = 0,Sum1 = 0,Sum2 = 0;
        while(Q.size())
        {
            int size = Q.size();
            level++;
            for(int i = 0;i<size;++i)
            {
                TreeNode* temp = Q.front();Q.pop();
                if(temp->left) Q.push(temp->left);
                if(temp->right) Q.push(temp->right);
                if(level%2!=0) {Sum1+=temp->val;cout<<"1"<<endl;}
                else {Sum2+=temp->val;cout<<"0"<<endl;}
            }
        }
        return Sum1>Sum2?Sum1:Sum2;
    }
};

最后发现还是年轻了 

算法参考:https://leetcode-cn.com/problems/house-robber-iii/solution/shu-xing-dp-ru-men-wen-ti-by-liweiwei1419/

本题的dp设法和股票问题有些相似,股票问题设置买不买,本题设置偷不偷

那么该对该根结点只有两个选择:

  1. 偷了!那么显然,该结点的孩子结点,不能偷
  2. 不偷,那么我们就去偷孩子结点

直接看程序:

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> Rob(2,0);
        Rob = DFS(root);
        return max(Rob[0],Rob[1]);
    }
    vector<int> DFS(TreeNode* root)
    {
        vector<int> Res(2,0);
        if(root == nullptr) return Res;//这就是本题的base case,当没有时,也就没有的偷,返回两种状态都是0
        vector<int>L(2,0); 
        L = DFS(root->left);
        vector<int>R(2,0); 
        R = DFS(root->right);

        //选择不偷,那么选出左右孩子结点偷与不偷的最大值即可
        Res[0] = max(L[0],L[1]) + max(R[0],R[1]);
        //选择偷,那么根结点的值,加上不偷孩子结点的选择
        Res[1] = root->val + L[0] + R[0];
        return Res;
    }
};

卡特兰数 

96. 不同的二叉搜索树

https://leetcode-cn.com/problems/unique-binary-search-trees/

本题是借着树的名义,进行动态规划

class Solution {
public:
    int numTrees(int n) {
        if(n == 0) return 0;
        vector<int> dp(n+1,0);
        //长度为n的序列的不同二叉搜索树个数(和序列内容无关,只和长度有关)
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2;i<=n;++i)//长度为i的序列
        {
            for(int j = 1;j<=i;++j)//长度为n的序列元素
            {
                dp[i] += dp[j-1]*dp[i-j];
                //j为根时,以j为根,将序列分为两个部分,两部分的所有可能相乘
            }
        }
        return dp[n];
    }
};

可以参考一下参考解法中的内容,本题就是一个数学问题,以第j个元素为根,然后左右子树的组合相互乘积得到的结果就是以

第j个元素为根所能组成二叉搜索树的结果。

本题其实也应用到了二叉搜索树的性质,以j为根,j前面的都比j小,都在j的左孩子树,右边都比j大,都是j的右孩子树。

参考解答:https://leetcode-cn.com/problems/unique-binary-search-trees/solution/hua-jie-suan-fa-96-bu-tong-de-er-cha-sou-suo-shu-b/

95. 不同的二叉搜索树 II

https://leetcode-cn.com/problems/unique-binary-search-trees-ii/

本题难度升级,你现在需要写出全部的可能性

class Solution {
public:
    vector<TreeNode*> generateTrees(int n) {
        if(n == 0) return {};
        return GetRes(1,n);  
    }
    //函数的功能,输入起点和终点,然后返回所有组合的数组
    vector<TreeNode*> GetRes(int begin,int end){
        //输入起点和终点,表示这段二叉搜索树的开始元素和结束元素
        if(begin > end) return {nullptr};//begin == end 相等的情况 要做保留
        vector<TreeNode*>Res;//此处要压入结果
        for(int i = begin;i<=end;++i){//i表示根结点的元素
            vector<TreeNode*>Left = GetRes(begin,i-1);//从根结点开始左右分开
            vector<TreeNode*>Right = GetRes(i+1,end);//从跟结点开始分割
            //开始排列组合
            for(auto L:Left){
                for(auto R:Right){
                    TreeNode* node = new TreeNode(i);//每种组合都必须新创建一个结点
                    node->left = L;
                    node->right = R;
                    Res.push_back(node);
                }
            }
        }
        return Res;
    }
};

 

 

 

 

再次非常感谢上述内容提供者无私的分享!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Leetcode动态规划部分典型题目分类及总结 的相关文章

  • 基础算法题——异或和之和(位运算、组合数)

    异或和之和 题目链接 解题思路 解题方案 暴力枚举 时间复杂度 O n3 超时 位操作 组合数 解铃还须系铃人 对于这种与 或 异或的位操作 一般也是通过位操作来解答 总结规律 题目要求在 n 个正整数中枚举 3 个数进行位操作 若要确定
  • Spring Boot v2.4.4源码解析(十)依赖注入原理下 —— 依赖解析

    从 Spring Boot v2 4 4源码解析 八 依赖注入原理上 由一道面试题引起的思考 Autowired和 Resource的区别 可以看出 解析待注入字段或参数主要由 org springframework beans facto
  • windows与linux字符集转换

    linux下不能显示windows下的汉字 Windows和LINUX中缺省使用的字符集不同 Windows下工具可以识别LINUX中使用的UTF8字符集 而LINUX下一般工具不会自动转换Windows下的GBK字符集 如果确实需要显示

随机推荐

  • Mysql数据备份及数据恢复

    数据备份概述 根据数据备份的方式 分为逻辑备份和物理备份 物理备份 物理备份就是备份数据文件了 比较形象点就是cp下数据文件 但真正备份的时候自然不是的cp这么简单 逻辑备份 将数据导出至文本文件中 mysqldump是采用SQL级别的备份
  • IDEA配置Scala,使用IDEA创建第一个Scala项目

    1 首先安装IDEA IDEA的官网 https www jetbrains com idea download section mac 选择你对应的系统 版本选择社区版即可 如果有需要的可以选择Ultimate版 2 安装后 打开IDEA
  • java常用类及其方法使用StringBuffer

    基本介绍 1 StringBuffer类是对String类的增强 其代表了可变字符序列 可以对字符串的内容进行增删 2 很多方法和String是相同的 但是StringBuffer是可变长度的 3 StringBuffer是一个容器 4 类
  • stata做计量入门常用代码一览!

    现在越来越多人有写论文的需求啦 经管领域的论文中 实证研究已成为必备操作 有了下面的代码 直接上手跑数据 一 分组回归 实证中 常常要分行业分年度 分省份分年度等分组回归 保存出回归出来的某些参数 statsby就是一个有用的命令 命令语句
  • MySQL基础——多表查询练习题

    文章目录 1 查询员工的姓名 年龄 部门信息 隐式内连接 2 查询年龄小于30岁的员工的姓名 年龄 职位 部门信息 显示内连接 3 查询拥有员工的部门id 部门名称 4 查询所有年龄大于40岁的员工 及其归属的部门名称 如果没有分配部门 也
  • 人工智能ai算法_AI算法比您想象的要脆弱得多

    人工智能ai算法 In William Gibson s 2010 novel Zero History a character preparing to go in a high stakes raid wears an oddly pa
  • window像mac一样使用快捷键(AutoHotkey、SharpKeys和PowerToys)

    自己有win和mac两台笔记本 每天都需要在两台电脑切换进行开发 快捷键的差异就让人很难受 个人喜好mac快捷键 常用的几个快捷键分布比较合理 所以网上找来了解决方案供大家参考 我想作为一名 Mac User 使用 Win 首先感到不适应的
  • ####好好好#####时序数据库介绍和使用

    1 基础 1 1 时序数据的定义 什么是时间序列数据 Time Series Data TSD 以下简称时序 从定义上来说 就是一串按时间维度索引的数据 用描述性的语言来解释什么是时序数据 简单的说 就是这类数据描述了某个被测量的主体在一个
  • webpack3 CommonsChunkPlugin插件分离三方库(jQuery.js/vue.js等)和公共模块 分别打包

    需求 使用webpack进行打包时 我们不想自己写的js文件与第三方的js库一起打包成一个庞大的文件 而是想要第三方插件库单独打包一个js 我们自己写的js独立打包 优点 1 分割js文件避免单独一个js文件太大影响用户使用体验 2 通常来
  • fastdfs特点

    FastDFS是一个开源的轻量级分布式文件系统 它对文件进行管理 功能包括 文件存储 文件同步 文件访问 文件上传 文件下载 等 解决了大容量存储和负载均衡的问题 特别适合以文件为载体的在线服务 如相册网站 视频网站等等 FastDFS为互
  • 每个开发人员都应该知道的10个CSS选择器

    对于任何网站而言 要在用户上产生良好印象是什么 是的 它是任何网站的用户界面 每个开发人员都知道为用户创建美观的设计以便与任何网站进行交互非常重要 如果你不熟悉CSS及其选择器 那么在最短的时间内巧妙地对网页进行样式设置并不是一件容易的事
  • css中display属性作用大全

    定义和用法 display 属性规定元素应该生成的框的类型 实例 设置段落生成一个行内框 p inline display inline 使用说明 说明 这个属性用于定义建立布局时元素生成的显示框类型 对于 HTML 等文档类型 如果使用
  • 学习记录,利用canvas写扫雷游戏

    记录js学习后制作的第一关小游戏 这里的代码还不够精简 许多地方偷懒没有封装 逻辑也有许多可以优化 胜利条件 找出所有地雷并标记
  • XSS-labs靶场1-13关解法答案

    目录 XSS labs克隆 下载地址 第一关 解法 第二关 解法 第三关 解法 第四关 解法 第五关 解法 第六关 解法 第七关 解法 第八关 解法 第九关 解法 第十关 解法 第十一关 解法 第十二关 解法 第十三关 解法 从XSS pa
  • 关于利用python解压文件出现文件名乱码的问题

    利用python中的zipfile模块解压zip文件会出现文件名乱码的问题 会成为莫名的字符 例如 出现原因 zipfile模块中的源代码如下 if flags 0x800 UTF 8 file names extension filena
  • C++异常详解

    文章目录 前言 一 C语言传统的处理错误的方式 二 C 异常概念 三 异常的使用 3 1 异常的抛出和捕获 3 2 异常的重新抛出 3 3 异常安全 3 4 异常规范 四 C 标准库的异常体系 五 自定义异常体系 六 异常的优缺点 C 异常
  • 创建线程的第三种方式:实现Callable接口(含部分源码解析)

    创建线程的第三种方式 实现Callable接口 package com lqy Multithreading import java util concurrent Callable import java util concurrent
  • 递归和迭代的区别

    http blog csdn net laoyang360 article details 7855860 http www zhihu com question 20278387 深究递归和迭代的区别 联系 优缺点及实例对比 1 概念区分
  • JavaWeb核心技术(上)

    目录 第一章 Servlet核心技术 上 前端相关 1 1 基本概念 常识 1 1 1 C S架构的概念 1 1 2 B S架构的概念 1 1 3 JavaWeb的概念 1 2 HTTP协议 熟悉 1 2 1 HTTP协议的概念 1 2 2
  • Leetcode动态规划部分典型题目分类及总结

    参考内容 https leetcode cn com problems longest palindromic substring solution zhong xin kuo san dong tai gui hua by liweiwe