509. 斐波那契数

image.png

思路一:递归

代码很简单

  1. class Solution {
  2. public:
  3. int fib(int n) {
  4. if (n < 2) return n;
  5. return fib(n - 1) + fib(n - 2);
  6. }
  7. };

但是可以用一个数组来保存几经计算过的数值,避免重复计算,降低时间复杂度
带备忘录的递归:

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

        return helper(n, dp);
    }
};

思路二:迭代

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

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

通过只维护两个变量来降低空间复杂度

class Solution {
public:
    int fib(int n) {
        if (n < 2) return n;
        int pre1 = 1, pre2 = 0;
        int res;
        for (int i = 2; i <= n; i++) {
            res = pre2 + pre1;
            pre2 = pre1;
            pre1 = res;
        }
        return res;
    }
};

70. 爬楼梯

image.png
这道题和斐波那契数其实是一样的,每个楼梯都有两种方式到达,从前两节跳两步,从前一节跳一步,于是有了状态转移方程
基础问题 - 图3
初始状态,dp[1] = 0, dp[2] = 2(从地上一次两阶,或者一次一阶跳两次)

直接递归会超时,不多赘述,代码和斐波那契也差不多

用dp数组

class Solution {
public:
    int climbStairs(int n) {
        //用递归会超时,改用自底向上法
        if (n == 1 || n == 2)
            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 || n == 2)
            return n;
        int pre2 = 1, pre1 = 2, cur;
        for (int i = 2; i < n; i++) { // i只代表迭代次数 i=2只当前在第三节
            cur = pre1 + pre2;
            pre2 = pre1;
            pre1 = cur;
        }
        return cur;
    }
};

746. 使用最小花费爬楼梯

image.png

解决动态规划问题的五个步骤:

代码:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        vector<int> dp(n + 1, 0); // cost共n个,下标n代表楼层顶部
        for (int i = 2; i < n + 1; i++) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[n]; 
    }
};

优化空间复杂度

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int pre1 = 0, pre2 = 0;
        int res;
        for (int i = 2; i <= cost.size(); i++) {
            res = min(pre1 + cost[i - 1], pre2 + cost[i - 2]);
            pre2 = pre1;
            pre1 = res;
        }
        return res; 
    }

62. 不同路径

image.png
image.png

动态规划

这道题是一个二维的动态规划问题,用一个二维数组dp来记录到达每个位置有几种方法,那么可以得到状态转移公式
基础问题 - 图7
表示当前位置可以由左面向右走一步或者由上面向下走一步到达。

初始状态:
dp[0][0] = 1,dp[i][1] = 1, dp[1][j] = 1

然后从左到右一层一层的遍历就可以得到完整的dp数组
最后返回dp[m][n]即可

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 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];
    }
};

优化空间(滚动数组):

只用一个一维数组dp来记录每一行的结果,因为我们以一行一行的遍历,因为未更新这个一维数组之前,其保存的是上一行的结果。在二维dp中,dp[i][j]为上面和左面的元素之和,使用一个数组,dp未更新前,dp[i]为上一行的结果,dp[i-1]更新后,保存的是左边的结果,那么状态转移方程为:
基础问题 - 图8

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n + 1, 0);
        dp[1] = 1;
        for (int i = 1; i <= m; ++i)
            for (int j = 1; j <= n; ++j)
                dp[j] += dp[j - 1];
        return dp[n];
    }
};

image.png
优化后空间复杂度为O(n)

思路三:数论方法

从起点走到终点,一共需要 m + n - 2步(其中 m-1步向下,n-1步向右)
那么问题可以转化为,从 m + n - 2 中,选择 m - 1 步向下,有多少种选法,那么答案为基础问题 - 图10
其中:
基础问题 - 图11

求组合的时候,要防止两个int相乘溢出! 所以不能把算式的分子都算出来,分母都算出来再做除法。

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
            ans = ans * x / y;
        }
        return ans;
    }
};

image.png

63. 不同路径 II

image.png
image.png
image.png
这道题相较于62题多了障碍物,
image.png
也就是说,障碍物右边或者下边的的位置,被封锁了一条路,在更新dp数组时,只能加一个数,也就是
dp[1][2] = 0 + dp[0][2],dp[2][1] = dp[0][2] + 0
那么将有障碍物的位置,保持初始值0,即可。如果有一个倒霉蛋,上面是障碍物,下面也是障碍物,那么就到不了这个位置了。

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        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 i = 0; i < n && obstacleGrid[0][i] == 0; i++) dp[0][i] = 1;
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 0) { // 只更新没有障碍物的地方
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];
    }
};

利用滚动数组优化:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        vector<int> dp(n + 1, 0);
        dp[1] = obstacleGrid[0][0] ? 0 : 1;
        for (int i = 1; i <= m; ++i)
            for (int j = 1; j <= n; ++j)
                if (obstacleGrid[i - 1][j - 1] == 0) // 没有障碍物更新
                    dp[j] += dp[j - 1];
                else
                    dp[j] = 0;  // 有障碍物则更新为0

        return dp[n];
    }
};

image.png

64. 最小路径和

image.png
在原数组上操作:

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

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

        return dp[m - 1][n - 1];
    }
};

优化空间复杂度后:
因为要求最小值,需要将dp数组初始化为INT32_MAX

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<int> dp(n + 1, INT32_MAX); // 求最小值一定要将初值设的大一点
        dp[1] = 0;
        for (int i = 0; i < m; ++i)
            for (int j = 1; j <= n; ++j)
                dp[j] = min(dp[j - 1], dp[j]) + grid[i][j - 1];
        return dp[n];
    }
};

343. 整数拆分

image.png

贪心

尽可能的多拆出3来,3越多,乘积越大

class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        int x = n % 3;
        int res;
        if (x == 0) {
            res = pow(3, n / 3);
        } else if (x == 1) {
            res = pow(3, n / 3 - 1) * 4;
        } else {
            res = pow(3, n / 3) * 2;
        }
        return res;
    }
};

动态规划

这道题就是切割钢条的思想
基础问题 - 图20
将数n拆分成 x, y
通过dp数组,记录1~n每个数字的最佳切割方案(记录x和y的最大乘积)
假设将 i 切为 j 和 i - j ,j 在 1 ~ i-1 之间迭代,那么状态转移方程为:
基础问题 - 图21

  • dp[i]:使用之前拆分乘积的最大值
  • (i - j) * j : 只拆为这两段
  • dp[i -j] * j:i - j 继续拆

定义一个dp数组,dp[i]表示整数i拆分后的最大乘积,1和0都不能拆分,dp[0]和dp[1]都是0

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1, 0);
        // 初始化0,1
        for (int i = 0; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                dp[i] = max(dp[i], max(j * dp[i - j], j * (i - j)));
            }
        }
        return dp[n];
    }
};

另一种写法(都一样):
对于每个dp[i]来说,假设拆分出一个 j 出来, 会有

//curMax记录对于不同拆分情况(遍历j)乘积的最大值
dp[i] = max(curMax,max(j * (i - j), j * dp[i - j]));
class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <= n; ++i) {
            int curMax = 0;
            for (int j = 1; j < i; ++j) {
                curMax = max(curMax, max(j * (i - j), j * dp[i - j]));
            }
            dp[i] = curMax;
        }
        return dp[n];
    }
};

赖皮做法,查表。因为 2 ≤ n ≤ 58

class Solution {
public:
    int integerBreak(int n) {
        vector<int> res{0, 0, 1, 2, 4, 6, 9, 12, 18, 27, 36, 54, 81, 108, 162, 243, 324, 486, 729, 972, 1458, 2187, 2916, 4374, 6561, 8748, 13122, 19683, 26244, 39366, 59049, 78732, 118098, 177147, 236196, 354294, 531441, 708588, 1062882, 1594323, 2125764, 3188646, 4782969, 6377292, 9565938, 14348907, 19131876, 28697814, 43046721, 57395628, 86093442, 129140163, 172186884, 258280326, 387420489, 516560652, 774840978, 1162261467, 1549681956};
        return res[n];
    }
};

96. 不同的二叉搜索树

image.png
因为求不同树的数量,并不用把搜索树都列出来,所以不用关心其具体数值的差异

动态规划

定义两个函数:
G(n): 长度为 n的序列能构成的不同二叉搜索树的个数。
F(i, n):以 i为根、序列长度为 nn 的不同二叉搜索树个数 (1≤i≤n)。
那么
基础问题 - 图23
image.png
每次以1~n中的不同节点 i 作为根的搜索树数量为 :以 1~i 能够构建搜索树的数量(左子树) 乘以 i~n 能够构建搜索树的数量(右子树)

基础问题 - 图25

两公式联立可以得到:
基础问题 - 图26

完整代码:

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++) { // 计算以j为根结点的搜索树数目
                dp[i] += dp[j - 1] * dp[i - j]; // 左子树的数量乘以右子树的数量就是以j为根结点的数目
            }
        }
        return dp[n];
    }
};

数学法

动态规划推导的基础问题 - 图27,是卡塔兰数 基础问题 - 图28

卡塔兰数:
基础问题 - 图29

class Solution {
public:
    int numTrees(int n) {
        long long C = 1;
        for (int i = 0; i < n; ++i) {
            C = C * 2 * (2 * i + 1) / (i + 2);
        }
        return (int)C;
    }
};

413. 等差数列划分

image.png
记dp[i]为以nums[i]为结尾的等差子数组个数,因为子数组最小为3,从第三个元素开始遍历,如果该元素与它前面的两个元素构成等差数列,那么dp[i] = dp[i - 1] + 1。解释:以nums[i - 1]为结尾的等差子数组个数为
dp[i - 1],那么这dp[i - 1]个子数组都可以加上nums[i]构成新的子数组,并且nums[i - 2]、nums[i - 1]、nums[i]也是一个等差子数组
最后返回的应该是dp的和

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& nums) {
        int n = nums.size(); 
        if (n < 3) return 0;
        vector<int> dp(n + 1, 0);//dp[i]表示从nums[0]到nums[i]的等差子数组的个数
        for (int i = 2; i < n; i++) {
            if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
                dp[i] = dp[i-1] + 1;
            }
        }
        return accumulate(dp.begin(), dp.end(), 0);
    }
};

或者,在更新dp时累加:

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

542. 01 矩阵

image.png
思路:从左上到右下,右下到左上,两次遍历矩阵,更新dp数组

class Solution {
public:
    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
        if (mat.empty()) return {};
        int n = mat.size(), m = mat[0].size();
        vector<vector<int>> dp(n, vector<int>(m, INT_MAX - 1));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (mat[i][j] == 0) {
                    dp[i][j] = 0;
                } else {
                    if (j > 0) {
                        dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
                    }
                    if (i > 0) {
                        dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
                    }
                }
            }
        }
        for (int i = n - 1; i >= 0; i--) {
            for (int j = m - 1; j >= 0; j--) {
                if (mat[i][j] != 0) {
                    if (j < m - 1) {
                    dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
                    }
                    if (i < n - 1) {
                    dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
                    }
                }
            }   
        }
        return dp;
    }
};

221. 最大正方形

image.png
image.png
对于在矩阵内搜索正方形或长方形的题型,一种常见的做法是定义一个二维 dp 数组,其中** dp[i][j] **表示满足题目条件的、以 (i, j) 为右下角的正方形或者长方形的属性
对于本题,则表示 以 (i, j) 为右下角的全由 1 构成的最大正方形面积。如果当前位置是 0,那么 dp[i][j] 即为 0;如果 当前位置是 1,我们假设 dp[i][j] = k2,其充分条件为 dp[i-1][j-1]、dp[i][j-1] 和 dp[i-1][j]的值必须 都不小于 (k k 1)2,否则 (i, j) 位置不可以构成一个边长为 k 的正方形。同理,如果这三个值中的 的最小值为 k k 1,则 (i, j) 位置一定且最大可以构成一个边长为 k 的正方形。

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if (matrix.empty() || matrix[0].empty())
            return 0;
        int m = matrix.size(), n = matrix[0].size(), max_side = 0;
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (matrix[i - 1][j - 1] == '1') {
                    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
                }
                max_side = max(max_side, dp[i][j]    );
            }
        }
        return max_side * max_side;
    }
};

拓展

image.png