代码随想录1刷—动态规划篇(一)

代码随想录1刷—动态规划篇(一)

      • 动态规划基础理论
        • 步骤
      • [509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/)
        • 动态规划解法
          • 优化
        • 递归解法
      • [70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/)
        • 拓展:完全背包问题
      • [746. 使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/)
      • [62. 不同路径](https://leetcode.cn/problems/unique-paths/)
        • 深搜解法(超时)
        • 动态规划
          • 优化
        • 数论解法(求组合的代码写法需要注意)
      • [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/)
      • [343. 整数拆分](https://leetcode.cn/problems/integer-break/)
        • 动态规划
        • 贪心解法
      • [96. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/)

动态规划基础理论

核心:动态规划中每一个状态一定是由上一个状态推导出来的。

步骤

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

509. 斐波那契数

动态规划解法

1、dp[i]的定义为:第i个数的斐波那契数值是dp[i]

2、状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];

3、初始化:dp[0] = 0;dp[1] = 1;

4、dp[i]是依赖 dp[i - 1] dp[i - 2],那么遍历的顺序一定是从前到后遍历的、

5、当N10的时候,dp数组应该是数列:0 1 1 2 3 5 8 13 21 34 55如果代码写出来,发现结果不对,就把dp数组打印出来看看和推导数列是否一致。

class Solution {
public:
    int fib(int n) {
        if(n <= 1) return n;
        vector<int> dp(n + 1);
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n; i++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
};

//时间复杂度:O(n);空间复杂度:O(n)
优化

当然可以发现,实际上只需要维护两个数值就可以了,不需要记录整个序列。

class Solution {
public:
    int fib(int N) {
        if (N <= 1) return N;
        int dp[2];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= N; i++) {
            int sum = dp[0] + dp[1];
            dp[0] = dp[1];
            dp[1] = sum;
        }
        return dp[1];
    }
};
//时间复杂度:O(n);空间复杂度:O(1)

递归解法

class Solution {
public:
    int fib(int N) {
        if (N < 2) return N;
        return fib(N - 1) + fib(N - 2);
    }
};
//时间复杂度:O(2^n);空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间

70. 爬楼梯

第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,所以可以用动态规划。

1、dp[i]: 爬到第i层楼梯,有dp[i]种方法

2、dp[i] = dp[i - 1] + dp[i - 2]

3、dp[1] = 1dp[2] = 2

4、从递推公式中可以看出,遍历顺序一定是从前向后遍历的

5、举例~懒得写了

class Solution {
public:
    int climbStairs(int n) {
        if(n <= 1) return n;
        vector<int> dp(n+1);
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i <= n;i++){
            dp[i] = dp[i - 1] + dp[i - 2]; 
        }
        return dp[n];
    }
};

//优化

class Solution {
public:
    int climbStairs(int n) {
        if(n <= 1) return n;
        int dp[3];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i <= n;i++){
            int sum = dp[1] + dp[2];
            dp[1] = dp[2];
            dp[2] = sum;
        }
        return dp[2];
    }
};

拓展:完全背包问题

一步一个台阶,两个台阶,三个台阶,直到 m 个台阶,有多少种方法爬到n阶楼顶?

class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) { 
                				// 把m换成2,就可以AC前面70.爬楼梯的题
                if (i - j >= 0) dp[i] += dp[i - j];
            }
        }
        return dp[n];
    }
};

746. 使用最小花费爬楼梯

1、dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]

2、dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];

3、dp[0] = cost[0];dp[1] = cost[1];

4、从前到后遍历cost数组

5、举例~懒得写了

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size());
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i = 2;i<cost.size();i++){
            dp[i] = min(dp[i-1],dp[i-2]) + cost[i];
        }
        return min(dp[cost.size()-1],dp[cost.size()-2]);    
        //到达最后一个台阶是不用花费的 所以取倒数第一和第二步的最少值即可。
    }
};
//优化
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int dp0 = cost[0];
        int dp1 = cost[1];
        for(int i = 2;i<cost.size();i++){
            int dpi = min(dp0,dp1) + cost[i];
            dp0 = dp1;
            dp1 = dpi;
        }
        return min(dp0,dp1);    
    }
};

62. 不同路径

深搜解法(超时)

class Solution {
private:
    int dfs(int i, int j, int m, int n) {
        if (i > m || j > n) return 0;   // 越界了
        if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点
        return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n);   //基于向下走or向右走后的方法
    }
public:
    int uniquePaths(int m, int n) {
        return dfs(1, 1, m, n);
    }
};  

动态规划

1、dp[i][j] :表示从(0 ,0)出发,到(i, j) dp[i][j]条不同的路径。

2、想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] dp[i][j - 1]

dp[i - 1][j]表示从(0, 0)的位置到(i - 1, j)几条路径,dp[i][j - 1]同理。

​ 所以:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

3、 dp[i][0] 一定都是1,因为从 (0, 0) 的位置到 (i, 0) 的路径只有一条,那么 dp[0][j] 也同理。

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

4、递归公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] dp[i][j] 都是从其上方和左方推导而来,那么从上到下从左到右遍历就可以了。

5、举例输出dp来验证~

代码随想录1刷—动态规划篇(一)_第1张图片
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m,vector<int>(n,0));
            // m: 3 和 n: 7 的情况下
            // dp: {{0, 0, 0, 0, 0, 0, 0}, 
            //      {0, 0, 0, 0, 0, 0, 0}, 
            //      {0, 0, 0, 0, 0, 0, 0}}
        for(int i = 0;i < m;i++){
            dp[i][0] = 1;
        }
        for(int j = 0;j < n;j++){
            dp[0][j] = 1;
        }
        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]; 
            }
        }
            //dp: {{1, 1, 1,  1,  1,  1,  1}, 
            //     {1, 2, 3,  4,  5,  6,  7}, 
            //     {1, 3, 6, 10, 15, 21, 28}}
        return dp[m-1][n-1];
    }
};
//时间复杂度:O(m × n);空间复杂度:O(m × n)
优化
//用一个一维数组(也可以理解是滚动数组)可以优化点空间
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n);
        for(int i = 0;i < n;i++){
            dp[i] = 1;
        }
        	//{1, 1, 1,  1,  1,  1,  1}, 
        for(int i = 1;i < m;i++){
            for(int j = 1;j < n;j++){
                dp[j] += dp[j-1]; 
            }
            // i = 1, {1, 2, 3,  4,  5,  6,  7}
            // i = 2, {1, 3, 6, 10, 15, 21, 28}
        }
        return dp[n-1];
    }
};
//时间复杂度:O(m × n);空间复杂度:O(n)

数论解法(求组合的代码写法需要注意)

一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。

所以答案就是: C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n2m1

注意:求组合的时候,要防止两个int相乘溢出! 也就是说不能把算式的分子都算出来,分母都算出来再做除法。需要在计算分子的时候,就去除以分母。

假设为 C 3 + 7 − 2 3 − 1 = C 8 2 = 8 ! 2 ! 6 ! = 7 ∗ 8 1 ∗ 2 C_{3+7-2}^{3-1}=C^{2}_{8}=\frac{8!}{2!6!}=\frac{7*8}{1*2} C3+7231=C82=2!6!8!=1278

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long numerator = 1; // 分子
        int denominator = m - 1; // 分母
        int count = m - 1;
        int t = m + n - 2;
        while (count--) {
            numerator *= (t--);		// 1*8 = 8 
            						// 4*7 = 28
            while (denominator != 0 && numerator % denominator == 0) {
                	//denominator = 0 说明分母的数字已经全部除过了 
                	//numerator % denominator == 0 是为了防止出现除不尽被系统直接取整的情况
                numerator /= denominator;	//  8/2=4 2-1=1 
                							//  4/1=4 1-1=0
                denominator--;
            }
        }
        return numerator;
    }
};
//时间复杂度:O(m);空间复杂度:O(1)

63. 不同路径 II

这道题相对于 62.不同路径 就是有了障碍。62.不同路径 中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table保持初始值0就可以了。

1、dp[i][j] :表示从(0 ,0)出发,到(i, j) dp[i][j]条不同的路径。

2、dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。

if (obstacleGrid[i][j] == 0) { 						// 当(i, j)没有障碍的时候,再推导dp[i][j]
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}

3、因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1dp[0][j]也同理。但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的,所以障碍之后的dp[i][0]是初始值0。

vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
//注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1(有障碍物)的情况就停止dp[i][0]的赋值为1条路径的操作,dp[0][j]同理。只有obstacleGrid[i][0] == 0(无障碍)的情况下才循环赋值。

4、递归公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] dp[i][j] 都是从其上方和左方推导而来,那么从上到下从左到右遍历就可以了。

5、举例一哈验证~

代码随想录1刷—动态规划篇(一)_第2张图片

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
            return 0;   //如果起点or终点出现了障碍物 直接返回
        }
        vector<vector<int>> dp(m,vector<int>(n,0));
        for(int i = 0; i < m && obstacleGrid[i][0] == 0; i++){
            dp[i][0] = 1;	//一旦出现障碍物,后续都为0,不再赋值为1
        }
        for(int j = 0; j < n && obstacleGrid[0][j] == 0; j++){
            dp[0][j] = 1;
        }
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
};

//时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(n × m)
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
            return 0;   //如果起点or终点出现了障碍物 直接返回
        }
        vector<int> dp(n);

        for(int j = 0; j < n; j++){         //第一层
            if(obstacleGrid[0][j] == 1){
                dp[j] = 0;                  //有障碍物
            }
            else{
                dp[j] = 1;                  //第一行全是1
            }
        }   

        for(int i = 1; i < m; i++){         //开始一层层
            for(int j = 0; j < n; j++){     //每层的列
                if(obstacleGrid[i][j] == 1){
                    dp[j] = 0;
                }
                else if(j != 0){
                    dp[j] = dp[j] + dp[j-1];
                }
            }
        }

        return dp[n-1];
    }
};
//优化
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
            return 0;   //如果起点or终点出现了障碍物 直接返回
        }
        vector<int> dp(n);

        for(int j = 0; j < n; j++){         //第一层
            if(obstacleGrid[0][j] == 1){
                dp[j] = 0;                  //有障碍物
            }
            else if(j == 0){
                dp[j] = 1;                  //第一个是1
            }
            else{
                dp[j] = dp[j - 1];          //后一个等于前一个
            }
        }   

        for(int i = 1; i < m; i++){         //开始一层层
            for(int j = 0; j < n; j++){     //每层的列
                if(obstacleGrid[i][j] == 1){
                    dp[j] = 0;  
                }
                else if(j != 0){     
                    //如果j == 0 && obstacleGrid[i][j] == 0,则每层的第一个会依赖于上一层的数,因此直接保留即可,不需要操作
                    dp[j] = dp[j] + dp[j-1];    //从第二列开始操作
                }
            }
        }

        return dp[n-1];
    }
};

//时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(m)

343. 整数拆分

动态规划

1、dp[i]:分拆数字i,可以得到的最大乘积为dp[i]

2、递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

3、初始化:dp[2] = 1

4、dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[2] = 1;
        for(int i = 3;i <= n;i++){
            for(int j = 1; j < i - 1;j++){
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
                //首先,外层的max是为了更新 在取i时拆分不同的j时的最大值
                //其次,内层的max中,(i - j) * j表示单纯的拆分为两个数相乘,而dp[i - j] * j表示拆分成两个以上(≥3)的个数相乘
            }
        }
        return dp[n];
    }
};

贪心解法

每次拆成n个3,剩下4及其以内的数则保留,然后相乘,这个结论需要数学证明其合理性!我不会证明,但确实合理哈哈哈,代码放下面啦,不证明了。

class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        int result = 1;
        while (n > 4) {
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }
};

96. 不同的二叉搜索树

1、dp[i]1i为节点组成的二叉搜索树的个数为dp[i]

2、递推公式: dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]j相当于是头结点的元素,从1遍历到i为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量。具体分析过程如下:

代码随想录1刷—动态规划篇(一)_第3张图片

3、从定义上来讲,空节点也是一棵二叉树也是一棵二叉搜索树,dp[0]= 1是可以说得通的;而从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0的话,乘法的结果就变成0了,而所有dp都需要基于dp[0],意味着后续所有dp全部变成0,所以能初始dp[0]=0,需要初始化dp[0] = 1

4、首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。那么遍历i里面每一个数作为头结点的状态,用j来遍历。

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for(int i = 1;i <= n;i++){   //遍历节点数
            for(int j = 1;j <= i;j++){  
                //遍历节点数内每一个数作为头结点,得到该数作为头结点的二叉搜索数的数目
                dp[i] += dp[j - 1] * dp[i - j]; //不断更新得到该节点数的二叉搜索树总数
            }
        }
        return dp[n];
    }
};

你可能感兴趣的:(笔试混子准备,动态规划,leetcode,算法)