力扣hot100——多维动态规划

文章目录

91.不同路径

题目描述

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 "Start" )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 "Finish" )。

问总共有多少条不同的路径?

示例 1:

复制代码
输入:m = 3, n = 7
输出:28

示例 2:

复制代码
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

复制代码
输入:m = 7, n = 3
输出:28

示例 4:

复制代码
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9

思路

  • 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]

代码

c 复制代码
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) 的路径数
}

92.最小路径和

题目描述

64. 最小路径和

给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

示例 1:

复制代码
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

复制代码
输入:grid = [[1,2,3],[4,5,6]]
输出:12

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200

思路:dp

  1. 定义状态
    • dp[i][j] 表示从 (0, 0)(i, j) 的最小路径和。
  2. 初始化
    • dp[0][0] = grid[0][0]:起点的最小路径和就是网格的第一个元素。
    • 对于第一行和第一列,路径只能从左边或上边移动,因此:
      • dp[i][0] = dp[i-1][0] + grid[i][0](第一列)。
      • dp[0][j] = dp[0][j-1] + grid[0][j](第一行)。
  3. 状态转移
    • 对于其他位置 (i, j),最小路径和为:
      • dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
  4. 返回结果
    • 返回 dp[m-1][n-1],即右下角的最小路径和。

代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int minPathSum(int** grid, int gridSize, int* gridColSize) {
    int m = gridSize;
    int n = gridColSize[0];
    if (m == 0 || n == 0) return 0; // 处理空网格

    // 定义 DP 数组
    int dp[m][n];
    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 j = 1; j < n; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }

    // 动态规划
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = fmin(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
        }
    }

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

93.最长回文子串

题目描述

5. 最长回文子串

  1. 定义状态
    • dp[i][j] 表示子串 s[i..j] 是否是回文子串。
  2. 初始化
    • 单个字符一定是回文子串,因此 dp[i][i] = true
  3. 状态转移
    • 对于长度大于 1 的子串 s[i..j]
      • 如果 s[i] == s[j],则 dp[i][j] 的值取决于 dp[i+1][j-1]
      • 如果 s[i] != s[j],则 dp[i][j] = false
  4. 记录最长回文子串
    • 遍历 dp 数组,找到最长的回文子串,并将其复制到结果字符串 ans 中。
  5. 返回结果
    • 返回 ans,即最长回文子串。

代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

char* longestPalindrome(char* s) {
    int n = strlen(s); // 获取字符串的长度
    if (n == 0) return ""; // 如果字符串为空,直接返回空字符串

    // 定义动态规划数组 dp[i][j],表示 s[i..j] 是否是回文子串
    bool dp[n][n];
    memset(dp, false, sizeof(dp)); // 初始化 dp 数组为 false

    // 单个字符一定是回文子串
    for (int i = 0; i < n; i++) {
        dp[i][i] = true;
    }

    int maxLen = 1; // 记录最长回文子串的长度
    int start = 0;  // 记录最长回文子串的起始位置

    // 检查长度为 2 的子串
    for (int i = 0; i < n - 1; i++) {
        if (s[i] == s[i + 1]) { // 如果两个相邻字符相等
            dp[i][i + 1] = true; // 则 s[i..i+1] 是回文子串
            maxLen = 2; // 更新最长回文子串的长度
            start = i;  // 更新最长回文子串的起始位置
        }
    }

    // 检查长度大于 2 的子串
    for (int len = 3; len <= n; len++) { // len 表示子串的长度
        for (int i = 0; i <= n - len; i++) { // i 表示子串的起始位置
            int j = i + len - 1; // j 表示子串的结束位置
            if (s[i] == s[j] && dp[i + 1][j - 1]) { // 如果首尾字符相等且内部子串是回文
                dp[i][j] = true; // 则 s[i..j] 是回文子串
                if (len > maxLen) { // 如果当前回文子串更长
                    maxLen = len; // 更新最长回文子串的长度
                    start = i;   // 更新最长回文子串的起始位置
                }
            }
        }
    }

    // 分配内存并复制结果
    char* ans = (char*)malloc(sizeof(char) * (maxLen + 1)); // 分配内存
    strncpy(ans, s + start, maxLen); // 复制最长回文子串
    ans[maxLen] = '\0'; // 添加字符串结束符

    return ans; // 返回结果
}

94.最长公共子序列

题目描述

1143. 最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

复制代码
输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

复制代码
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

复制代码
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1text2 仅由小写英文字符组成。

思路:dp

1. 状态定义

  • d p i j dpij dpij 表示 字符串 t e x t 1 text1 text1 的前 i i i 个字符字符串 t e x t 2 text2 text2 的前 j j j 个字符 之间的最长公共子序列长度。

2. 初始状态

  • 任何字符串和空字符串的最长公共子序列长度均为 0 0 0,即:
    d p i 0 = 0 , d p 0 j = 0 dpi0 = 0, \quad dp0j = 0 dpi0=0,dp0j=0

  • 代码中通过 memset(dp, 0, sizeof(dp)) 进行初始化。

3. 状态转移方程

对于 i ≥ 1 i \geq 1 i≥1 且 j ≥ 1 j \geq 1 j≥1,状态转移公式如下:
d p i j = { d p i − 1 j − 1 + 1 , if t e x t 1 i − 1 = t e x t 2 j − 1 max ⁡ ( d p i − 1 j , d p i j − 1 ) , if t e x t 1 i − 1 ≠ t e x t 2 j − 1 dpij = \begin{cases} dpi-1j-1 + 1, & \text{if } text1i-1 = text2j-1 \\ \max(dpi-1j, dpij-1), & \text{if } text1i-1 \neq text2j-1 \end{cases} dpij={dpi−1j−1+1,max(dpi−1j,dpij−1),if text1i−1=text2j−1if text1i−1=text2j−1

解释:

  • 匹配情况 :如果 t e x t 1 i − 1 = t e x t 2 j − 1 text1i-1 = text2j-1 text1i−1=text2j−1,说明当前字符匹配成功,
    • 则可以在之前的匹配基础上加 1 1 1,即 d p i j = d p i − 1 j − 1 + 1 dpij = dpi-1j-1 + 1 dpij=dpi−1j−1+1。
  • 不匹配情况 :如果 t e x t 1 i − 1 ≠ t e x t 2 j − 1 text1i-1 \neq text2j-1 text1i−1=text2j−1,则当前字符不匹配,
    • 需要取去掉当前字符后的最大值,即 d p i j = max ⁡ ( d p i − 1 j , d p i j − 1 ) dpij = \max(dpi-1j, dpij-1) dpij=max(dpi−1j,dpij−1)。

代码

c 复制代码
int longestCommonSubsequence(char* text1, char* text2) {
    // 获取字符串 text1 和 text2 的长度
    int m = strlen(text1), n = strlen(text2);
    
    // 定义一个二维 DP 数组,大小为 (m+1) x (n+1)
    // dp[i][j] 表示 text1 的前 i 个字符与 text2 的前 j 个字符之间的 LCS 长度
    int dp[m+1][n+1];
    
    // 将 dp 数组所有元素初始化为 0
    memset(dp, 0, sizeof(dp));
    
    // 遍历 text1 和 text2 的所有字符组合
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 如果当前字符相等,则可以将前面的 LCS 长度加 1
            if (text1[i-1] == text2[j-1])
                dp[i][j] = dp[i-1][j-1] + 1;
            else
                // 如果不相等,则选择删除其中一个字符,取较大值
                dp[i][j] = fmax(dp[i-1][j], dp[i][j-1]);
        }
    }
    
    // 返回两个字符串的 LCS 长度
    return dp[m][n];
}

95.编辑距离

题目描述

72. 编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

复制代码
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

复制代码
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成

思路:dp

583. 两个字符串的删除操作不同的是添加了插入和替换操作,其实插入和删除操作的效果是一样的,因此可以忽略添加的插入操作,只用删除和替换操作。

思路与上题相比,只是略有不同:

如果 word1[i-1] ≠ word2[j-1](当前字符不匹配),则有 3 种操作方式:

  • 删除 word1[i-1] (让 word1 变短):dp[i-1][j] + 1

  • 删除 word2[j-1] (使 word2 变短):dp[i][j-1] + 1

  • 替换 word1[i-1]word2[j-1]dp[i-1][j-1] + 1

  • 取最小值:
    d p i j = m i n ⁡ ( d p i − 1 j + 1 , d p i j − 1 + 1 , d p i − 1 j − 1 + 1 ) dpij=min⁡(dpi−1j+1,dpij−1+1,dpi−1j−1+1) dpij=min⁡(dpi−1j+1,dpij−1+1,dpi−1j−1+1)

其他都是相同的:

  • 动态规划定义dp[i][j] 表示 word1i 个字符变为 word2j 个字符的最小操作次数。

  • 状态转移方程
    d p i j = { d p i − 1 j − 1 , 如果 w o r d 1 i − 1 = w o r d 2 j − 1 min ⁡ ( d p i − 1 j − 1 + 1 , d p i − 1 j + 1 , d p i j − 1 + 1 ) , 否则 dpij = \begin{cases} dpi-1j-1, & \text{如果 } word1i-1 = word2j-1 \\ \min(dpi-1j-1 + 1, dpi-1j + 1, dpij-1 + 1), & \text{否则} \end{cases} dpij={dpi−1j−1,min(dpi−1j−1+1,dpi−1j+1,dpij−1+1),如果 word1i−1=word2j−1否则

  • 边界处理

    • dp[i][0] = i(删除 word1
    • dp[0][j] = j(插入 word2

代码

c 复制代码
#include <stdio.h>
#include <string.h>
#include <math.h>

int minDistance(char* word1, char* word2) {
    int m = strlen(word1);
    int n = strlen(word2);
    int dp[m+1][n+1];

    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i;  // 需要删除 i 个字符
    }
    for (int j = 0; j <= n; j++) {
        dp[0][j] = j;  // 需要插入 j 个字符
    }

    // 计算 dp[i][j] 状态
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i-1] == word2[j-1]) {  // 字符匹配,无需编辑
                dp[i][j] = dp[i-1][j-1];
            } else {  // 选择删除、插入或替换
                dp[i][j] = fmin(dp[i-1][j-1] + 1,   // 替换
                                fmin(dp[i-1][j] + 1,  // 删除
                                     dp[i][j-1] + 1)); // 删除
            }
        }
    }

    return dp[m][n];  // 返回最少编辑距离
}
相关推荐
春日见10 分钟前
决策规划控制面经汇总
人工智能·深度学习·算法·机器学习·自动驾驶
Full Stack Developme11 分钟前
Java DFA算法
java·python·算法
fie888920 分钟前
LBP + HOG 特征检测与识别 MATLAB 实现
数据结构·算法·matlab
海天鹰23 分钟前
图片去黑边算法
qt·算法
wabs66625 分钟前
关于动态规划【力扣63.不同路径II与62.不同路径的区别(C++)】自我总结
动态规划
xxwl5851 小时前
一个原创题(二)
c++·算法
moeyui7051 小时前
LeetCode 380:Insert Delete GetRandom O(1) 题解和一些延伸
算法·leetcode·职场和发展
三千里1 小时前
路径规划算法-备忘
算法·自动驾驶·动态规划
圣保罗的大教堂1 小时前
leetcode 3689. 最大子数组总值 I 中等
leetcode
退休倒计时1 小时前
【每日一题】LeetCode 15. 三数之和 TypeScript
数据结构·算法·leetcode·typescript