# 不同路径

题目链接🔗

机器人要从 (0, 0) 走到 (m-1, n-1)。

  1. 确定 dp 数组以及下标的含义

dp [i][j]: 从 (0, 0) 到 (i, j) 有 dp [i][j] 条不同的路径。

  1. 确定递推公式

dp 可以从两个方向推导出来,即 dp [i-1][j] 和 dp [i][j-1]。

那么dp[i][j]=dp[i1][j]+dp[i][j1]dp[i][j] = dp[i-1][j] + dp[i][j-1]

  1. dp 数组初始化

dp [i][0] 和 dp [0][j] 都为 0,因为从 (0,0) 到 (i,0) 或者 (0,j) 的路径只有一条

  1. 确定遍历顺序

dp [i][j] 是从上方和左方推导出来,那么从左到右一层一层遍历即可。

  1. 举例推导 dp 数组

Alt text

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

# 不同路径 II

题目链接🔗

和上题描述区别在于有障碍了,但动态规划思路不变。

在没有碰到障碍的时候,dp 数组正常的递推,碰到障碍就跳过。

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) // 如果在起点或终点出现了障碍,直接返回 0
            return 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 (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];
    }
};

# 整数拆分

题目链接🔗

  1. 确定 dp 数组以及下标的含义

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

  1. 确定递推公式

最大乘积是怎么来的。

j 是从 1 开始遍历到 i 的所有情况。

i 如果拆成两个数就是 j×(ij)j \times (i-j),如果拆成三个及以上就是 j×dp[ij]j \times dp[i-j]

j 是从 1 开始遍历的,因此在 j 的拆分在遍历中都包含了。

递推公式就为 dp[i]=max(dp[i],max((ij)j,dp[ij]j))dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))

  1. dp 的初始化

从 dp 数组的定义讲,dp [0] 和 dp [1] 没有意义不需要初始化。

从 dp [2] 开始,dp [2] = 1。

  1. 遍历顺序

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

i 从 3 开始遍历到 n,j 从 1 开始遍历遍历到i2\frac{i}{2}。因为拆分一个数 n 使之乘积最大,那么一定是拆分成 m 个近似相同的子数相乘才是最大的。

  1. 举例推导 dp 数组

Alt text

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 / 2; j++) {
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
            }
        }
        return dp[n];
    }
};

# 不同的二叉搜索树

题目链接🔗

n 为 1 和 n 为 2 很简单,从 n 为 3 开始:

父节点为 1,右子树有两个节点,和 n 为 2 时的节点布局一样。

父节点为 2,左右子树各有一个节点,和 n 为 1 时的节点布局一样。

父节点为 3,左子树有两个节点,和 n 为 2 时的节点布局一样。

dp [3],就是 元素 1 为头结点搜索树的数量 + 元素 2 为头结点搜索树的数量 + 元素 3 为头结点搜索树的数量

元素 1 为头结点搜索树的数量 = 右子树有 2 个元素的搜索树数量 * 左子树有 0 个元素的搜索树数量

元素 2 为头结点搜索树的数量 = 右子树有 1 个元素的搜索树数量 * 左子树有 1 个元素的搜索树数量

元素 3 为头结点搜索树的数量 = 右子树有 0 个元素的搜索树数量 * 左子树有 2 个元素的搜索树数量

有 2 个元素的搜索树数量就是 dp [2]。

有 1 个元素的搜索树数量就是 dp [1]。

有 0 个元素的搜索树数量就是 dp [0]。

所以 dp [3] = dp [2] * dp [0] + dp [1] * dp [1] + dp [0] * dp [2]

  1. 确定 dp 数组以及下标含义

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

或者 i 个不同元素节点组成的二叉搜索树的个数。

  1. 确定递推公式

用 j 相当于头结点元素,从 1 开始遍历到 i。

dp [i] += dp [以 j 为头结点左子树节点数量] * dp [以 j 为头结点右子树节点数量]

因为是二叉搜索树,所以 j 的左子树节点数为 j-1,右子树节点为 i-j。

递推公式:dp[i]+=dp[j1]dp[ij]dp[i] += dp[j - 1] * dp[i - j]

  1. dp 的初始化

初始化 dp [0] = 1,推导的基础都是 dp [0]。

  1. 确定遍历顺序

节点数为 i 的状态是依靠 i 之前节点数的状态。

那么遍历 i 里面每一个数作为头结点的状态,用 j 来遍历。

  1. 举例推导 dp 数组

Alt text

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];
    }
};