代码随想录——动态规划(基础题)

斐波那契数

509. 斐波那契数

思路:dp/递归
  • dp数组含义:f[i]表示fib[i]的值;
  • 递推公式:f[i]=f[i-1]+f[i-2];
  • 初始化:f[0]=0,f[1]=1;
  • 遍历顺序:i=0···n
代码
int fib(int n) {
    // 基本情况:当 n 为 0 或 1 时,直接返回 n
    if (n <= 1) return n;

    // 创建一个大小为 n+1 的数组,用于存储斐波那契数列
    int f[n + 1];

    // 初始化前两个斐波那契数
    f[0] = 0; // fib(0) = 0
    f[1] = 1; // fib(1) = 1

    // 通过动态规划计算斐波那契数列
    for (int i = 2; i <= n; i++) {
        // 当前值等于前两个值之和
        f[i] = f[i - 1] + f[i - 2];
    }

    // 返回第 n 个斐波那契数
    return f[n];
}

爬楼梯

70. 爬楼梯

思路:dp
1. dp数组含义
  • dp[i] 表示爬到第 i 层楼梯的方法数。
  • 例如:
    • dp[0] 表示爬到第 0 层(地面)的方法数。
    • dp[1] 表示爬到第 1 层的方法数。
    • dp[2] 表示爬到第 2 层的方法数。
2. 递推公式
  • 每次只能爬 1 步或 2 步,因此:

    • 爬到第 i 层的方法数 = 爬到第 i-1 层的方法数 + 爬到第 i-2 层的方法数。
  • 即:

    dp[i]=dp[i−1]+dp[i−2]

3. 初始化
  • 爬到第 0 层(地面)只有一种方法,就是不爬,因此:

    dp[0]=1

  • 爬到第 1 层只有一种方法,就是爬一步,因此:

    dp[1]=1

4. 遍历顺序
  • 从第 2 层开始,依次计算每一层的方法数。
  • 因为 dp[i] 依赖于 dp[i-1]dp[i-2],所以需要从小到大遍历。
5. 返回值
  • 最终返回 dp[n],即爬到第 n 层的方法数。
代码
int climbStairs(int n) {
    // dp数组含义:dp[i] 表示爬到第 i 层楼梯的方法数
    int dp[n + 1];

    // 初始化:爬到第 0 层和第 1 层的方法数都是 1
    dp[0] = 1; // 爬到第 0 层(地面)只有一种方法,就是不爬
    dp[1] = 1; // 爬到第 1 层只有一种方法,就是爬一步

    // 遍历顺序:从第 2 层开始,依次计算爬到每一层的方法数
    for (int i = 2; i <= n; i++) {
        // 递推公式:爬到第 i 层的方法数等于爬到第 i-1 层的方法数加上爬到第 i-2 层的方法数
        // 因为每次只能爬 1 步或 2 步,所以第 i 层只能从第 i-1 层或第 i-2 层到达
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    // 返回结果:爬到第 n 层的方法数
    return dp[n];
}

使用最小花费爬楼梯

746. 使用最小花费爬楼梯

思路:dp
1. dp数组含义
  • dp[i] 表示从第 i 层(层数从0开始)楼梯出发,爬到楼顶的最小花费。
  • 例如:
    • dp[costSize-1] 表示从倒数第一层出发的最小花费。
    • dp[costSize-2] 表示从倒数第二层出发的最小花费。
2. 递推公式
  • 每次可以爬 1 步或 2 步,因此:

    • 从第 i 层出发的最小花费 = 当前层的花费 cost[i] + 下一步的最小花费 min(dp[i+1], dp[i+2])
  • 即:

    dp[i]=min⁡(dp[i+1],dp[i+2])+cost[i]

3. 初始化
  • 最后两层的花费可以直接从 cost 数组中获取:
    • dp[costSize-1] = cost[costSize-1]:从倒数第一层出发,花费为 cost[costSize-1]
    • dp[costSize-2] = cost[costSize-2]:从倒数第二层出发,花费为 cost[costSize-2]
4. 遍历顺序
  • 从倒数第三层开始,向前计算每一层的最小花费。
  • 因为 dp[i] 依赖于 dp[i+1]dp[i+2],所以需要从后向前遍历。
5. 返回值
  • 最终返回 min(dp[0], dp[1]),即从第 0 层或第 1 层出发的最小花费。
代码
int minCostClimbingStairs(int* cost, int costSize) {
    // dp数组含义:dp[i] 表示从第 i 层楼梯出发,爬到楼顶的最小花费
    int dp[costSize];

    // 初始化:最后两层的花费就是 cost 数组中的值
    dp[costSize - 1] = cost[costSize - 1]; // 从倒数第一层出发,花费为 cost[costSize-1]
    dp[costSize - 2] = cost[costSize - 2]; // 从倒数第二层出发,花费为 cost[costSize-2]

    // 遍历顺序:从倒数第三层开始,向前计算每一层的最小花费
    for (int i = costSize - 3; i >= 0; i--) {
        // 递推公式:从第 i 层出发的最小花费等于当前层的花费加上下一步的最小花费
        // 因为每次可以爬 1 步或 2 步,所以取 dp[i+1] 和 dp[i+2] 中的较小值
        dp[i] = fmin(dp[i + 1], dp[i + 2]) + cost[i];
    }

    // 返回结果:从第 0 层或第 1 层出发的最小花费
    return fmin(dp[0], dp[1]);
}

不同路径

思路:dp
  • dp 数组含义dp[i][j] 表示到达 (i, j) 的不同路径数。

  • 递推公式dp[i][j] = dp[i-1][j] + dp[i][j-1],即从上方和左方转移。

  • 初始化:第一行和第一列的路径数均为 1,因为机器人只能向右或向下走。

  • 遍历顺序:先初始化边界条件,再从 (1,1) 开始按行填充 dp 数组,最终返回 dp[m-1][n-1]

代码
int uniquePaths(int m, int n) {
    // 1. dp 数组含义:
    //    dp[i][j] 表示从起点 (0,0) 到达 (i,j) 的不同路径数
    if (m == 1 || n == 1) return 1; // 如果 m 或 n 为 1,则只有一条路径可走
    
    int dp[m][n]; // 定义 dp 数组,存储每个位置的路径数
    
    // 2. dp 数组初始化:
    //    由于机器人只能向右或向下移动,所以第一列和第一行的路径数都是 1
    for (int i = 0; i < m; i++) dp[i][0] = 1; // 第一列所有位置的路径数均为 1
    for (int i = 1; i < n; i++) dp[0][i] = 1; // 第一行所有位置的路径数均为 1
    
    // 3. dp 递推公式:
    //    机器人可以从上方 (i-1, j) 或左方 (i, j-1) 到达 (i, j)
    //    因此,dp[i][j] = dp[i-1][j] + dp[i][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]; // 递推计算
        }
    }
    
    // 4. dp 遍历顺序:
    //    - 先填充第一列和第一行(初始化)
    //    - 再从 (1,1) 开始按行遍历整个 dp 数组,每个位置由上方和左方的值推导得出
    return dp[m - 1][n - 1]; // 返回终点 (m-1, n-1) 的路径数
}

不同路径Ⅱ

63. 不同路径 II

思路:二维dp
dp 数组含义
  • dp[i][j] 代表从起点 (0,0) 到达 (i,j) 的不同路径数,考虑了障碍物的情况。
递推公式
  • 如果 (i, j) 位置是障碍物dp[i][j] = 0

  • 否则,到达 (i,j) 的路径数为上方和左方路径之和:

    dp[i][j]=dp[i-1][j]+dp[i][j-1]

初始化
  • 起点 (0,0) 是障碍物,则直接返回 0(无路径可达)。
  • 第一列:如果 (i,0) 是障碍物,后续列都不可达;否则继承上方路径。
  • 第一行:如果 (0,j) 是障碍物,后续行都不可达;否则继承左侧路径。
遍历顺序
  • 先初始化第一行、第一列(保证第一步的可达性)。
  • (1,1) 开始遍历整个 dp 数组,依赖于上方和左方的路径数进行递推。
代码
int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize) {
    int m = obstacleGridSize;  // 行数
    int n = obstacleGridColSize[0];  // 列数
    
    int dp[m][n];  // 定义 dp 数组,存储到达每个位置的路径数
    
    // 1. dp 数组含义:
    //    dp[i][j] 表示从起点 (0,0) 到达 (i,j) 的不同路径数(考虑障碍物)
    
    // 2. dp 数组初始化:
    //    - 如果起点 (0,0) 有障碍物,则直接返回 0
    if (obstacleGrid[0][0] == 0) dp[0][0] = 1;
    else return 0; // 起点有障碍物,直接无路可走
    
    // 初始化第一列:
    //    - 如果某一格是障碍物,之后的所有格子都不可达
    for (int i = 1; i < m; i++) {
        if (obstacleGrid[i][0] == 0) dp[i][0] = dp[i - 1][0]; // 沿着列继承路径
        else dp[i][0] = 0; // 障碍物位置路径数为 0
    }
    
    // 初始化第一行:
    for (int i = 1; i < n; i++) {
        if (obstacleGrid[0][i] == 0) dp[0][i] = dp[0][i - 1]; // 沿着行继承路径
        else dp[0][i] = 0; // 障碍物位置路径数为 0
    }
    
    // 3. dp 递推公式:
    //    - 只有当前格子不是障碍物,才能继承路径
    //    - 继承方式:
    //      dp[i][j] = dp[i-1][j] + dp[i][j-1],如果 (i, j) 不是障碍物
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (obstacleGrid[i][j] == 1) dp[i][j] = 0; // 如果是障碍物,则路径数为 0
            else dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 否则继承上方和左方的路径数
        }
    }
    
    // 4. dp 遍历顺序:
    //    - 先初始化第一行和第一列
    //    - 再从 (1,1) 开始按行遍历整个 dp 数组
    return dp[m - 1][n - 1]; // 返回终点 (m-1, n-1) 的路径数
}

整数拆分

343. 整数拆分

思路:dp
1. dp数组含义
  • dp[i] 表示将整数 i 拆分成至少两个正整数的和,这些整数的乘积的最大值(也可以不拆,即最小值为i)。
  • 例如:
    • dp[2] 表示将 2 拆分的最大乘积。
    • dp[3] 表示将 3 拆分的最大乘积。
2. 递推公式
  • 对于每个整数 i,可以将其拆分为 ji-j,其中 1 <= j < i

  • 拆分的最大乘积为 dp[j] * dp[i-j]

  • 因此,递推公式为:

    dp[i]=max⁡(dp[i],dp[j]×dp[i−j])

  • 需要遍历所有可能的 j,找到最大值。

3. 初始化
  • 对于 1, 2, 3,拆分的最大乘积为它们本身:
    • dp[1] = 1:1 无法拆分,乘积为 1。
    • dp[2] = 2:2 拆分为 1+1,但最大乘积为 2(不拆分)。
    • dp[3] = 3:3 拆分为 1+2,但最大乘积为 3(不拆分)。
4. 遍历顺序
  • i = 4 开始,逐个计算 dp[i]
  • 对于每个 i,遍历 j1i-1,计算 dp[j] * dp[i-j] 的最大值。
5. 返回值
  • 最终返回 dp[n],表示将 n 拆分的最大乘积。
代码一
int integerBreak(int n) {
    // dp数组含义:dp[i] 表示将整数 i 拆分成至少两个正整数的和,这些整数的乘积的最大值
    int dp[n + 1];
    memset(dp, 0, sizeof(dp)); // 初始化 dp 数组为 0

    // 特殊情况处理
    if (n == 2) return 1; // 2 只能拆分为 1+1,乘积为 1
    if (n == 3) return 2; // 3 只能拆分为 1+2,乘积为 2

    // 初始化:对于 1, 2, 3,拆分的最大乘积为它们本身
    dp[1] = 1; // 1 无法拆分,乘积为 1
    dp[2] = 2; // 2 拆分为 1+1,但最大乘积为 2(不拆分)
    dp[3] = 3; // 3 拆分为 1+2,但最大乘积为 3(不拆分)

    // 遍历顺序:从 4 开始,逐个计算 dp[i]
    for (int i = 4; i <= n; i++) {
        // 对于每个 i,尝试将其拆分为 j 和 i-j,计算 dp[j] * dp[i-j] 的最大值
        for (int j = 1; j < i; j++) {
            dp[i] = fmax(dp[i], dp[j] * dp[i - j]);
        }
    }

    // 返回结果:dp[n] 表示将 n 拆分的最大乘积
    return dp[n];
}
代码二
  • dp数组含义:dp[i] 表示将整数 i 拆分成至少两个正整数的和,这些整数的乘积的最大值

  • 递推公式:dp[i]=max(dp[i],max(j×(i−j),j×dp[i−j]))

int integerBreak(int n) {
    // dp数组含义:dp[i] 表示将整数 i 拆分成至少两个正整数的和,这些整数的乘积的最大值
    int dp[n + 1];
    memset(dp, 0, sizeof(dp)); // 初始化 dp 数组为 0

    // 初始化:0 和 1 无法拆分,乘积为 0
    dp[0] = 0; // 0 无法拆分,乘积为 0
    dp[1] = 0; // 1 无法拆分,乘积为 0

    // 遍历顺序:从 2 开始,逐个计算 dp[i]
    for (int i = 2; i <= n; i++) {
        // 对于每个 i,尝试将其拆分为 j 和 i-j,计算最大乘积
        for (int j = 1; j < i; j++) {
            // 递推公式:dp[i] = max(dp[i], max(j * (i-j), j * dp[i-j]))
            // j * (i-j) 表示将 i 拆分为 j 和 i-j,不继续拆分 i-j
            // j * dp[i-j] 表示将 i 拆分为 j 和 i-j,并继续拆分 i-j
            dp[i] = fmax(dp[i], fmax(j * (i - j), j * dp[i - j]));
        }
    }

    // 返回结果:dp[n] 表示将 n 拆分的最大乘积
    return dp[n];
}

不同的二叉搜索树

96. 不同的二叉搜索树

思路:dp
1.dp数组含义

dp[i]表示使用i个节点时,能够形成的不同二叉搜索树的数量。

2.dp数组递推公式

对于每个节点数i,根节点可以是1到i之间的任意一个节点。假设根节点是第j个节点,那么:

  • 左子树的节点数是j-1,其排列方式数是dp[j-1]
  • 右子树的节点数是i-j,其排列方式数是dp[i-j]

因此,dp[i]可以通过遍历所有可能的根节点j,累加每种情况的乘积:

d p [ i ] = ∑ j = 1 i ( d p [ j − 1 ] × d p [ i − j ] ) dp[i] = \sum_{j=1}^{i} (dp[j-1] \times dp[i-j]) dp[i]=j=1i(dp[j1]×dp[ij])

3.dp数组初始化
  • dp[0] = 1:空树只有一种可能,即什么都不做。
  • dp[1] = 1:只有一个节点的树只有一种结构。
4.dp数组遍历顺序

外层循环从2到n,遍历所有节点数i,内层循环从1到i,遍历所有可能的根节点j。对于每种根节点选择,计算其左右子树的组合数,并加到dp[i]中。

5.返回结果

最终返回dp[n],即n个节点能够形成的不同二叉搜索树的数量。

代码
int numTrees(int n) {
    // dp[i]表示有i个节点时,能够形成的不同二叉搜索树的个数
    int dp[n+1]; 
    
    // 初始化dp数组,所有元素都为0
    memset(dp, 0, sizeof(dp));
    
    // 基本情况:空树和只有1个节点的树各自只有1种情况
    dp[0] = 1;  // 空树有1种方式
    dp[1] = 1;  // 只有一个节点时有1种树形
    
    // dp数组的递推公式:dp[i] = Σ(dp[j-1] * dp[i-j]),j从1到i
    // 其中dp[j-1]表示左子树的不同排列方式,dp[i-j]表示右子树的不同排列方式
    for (int i = 2; i <= n; i++) {
        // 对每个节点数i,尝试以1到i作为根节点
        for (int j = 1; j <= i; j++) {
            // 对于每种选择的根节点j,将其左右子树的排列组合情况相乘,并累加到dp[i]
            dp[i] += dp[j-1] * dp[i-j];
        }
    }
    
    // 最终返回当节点数为n时,形成的不同二叉搜索树的数量
    return dp[n];
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值