动态规划 - Dynamic Programming - 2
最近更新:2025-10-20   |   字数总计:2.2k   |   阅读估时:10分钟   |   阅读量:
  1. 基础算法 - 动态规划 - 2
    1. 相关题目
      1. 62. 不同路径 - 力扣(LeetCode)
      2. 64. 最小路径和 - 力扣(LeetCode)
      3. 5. 最长回文子串 - 力扣(LeetCode)
        1. 动态规划
        2. 中心开花法
      4. 1143. 最长公共子序列 - 力扣(LeetCode)
      5. 72. 编辑距离 - 力扣(LeetCode)

基础算法 - 动态规划 - 2

多维动态规划。

相关题目

62. 不同路径 - 力扣(LeetCode)

难度:中等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m + 1][n + 1]; // 创建大小为 (m+1)x(n+1) 的二维数组
dp[0][1] = 1; // 初始化起点前一个位置为 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][n]; // 返回到达终点 (m, n) 的路径数量
}
}

爬楼梯二维版。

状态定义

dp[i][j] 表示从起点 (1, 1) 到位置 (i, j) 的路径数量。

状态转移方程

对于位置 (i, j),它的路径数量等于它上方位置 (i-1, j) 和左侧位置 (i, j-1) 的路径数量之和: dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

64. 最小路径和 - 力扣(LeetCode)

难度:中等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m + 1][n + 1];

for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 第一排 和第一列特殊处理
if (i == 1) {
dp[i][j] = dp[i][j - 1] + grid[i - 1][j - 1];
} else if (j == 1) {
dp[i][j] = dp[i - 1][j] + grid[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}
}
return dp[m][n];
}
}

状态定义

dp[i][j] 表示从起点 (1, 1) 到位置 (i, j) 的最小路径和。

状态转移

对于位置 (i, j),它的最小路径和等于它上方位置 (i-1, j) 和左侧位置 (i, j-1) 的最小路径和加上当前位置的值。

5. 最长回文子串 - 力扣(LeetCode)

难度:中等

动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public String longestPalindrome(String s) {
int len = s.length(); // 获取字符串的长度
boolean[][] dp = new boolean[len + 2][len + 2]; // 定义二维布尔数组,用于存储动态规划的状态

// 初始化dp数组,全部设为true(这是多余的,实际使用中可以直接省略这一步)
for (int i = 0; i <= len + 1; i++)
Arrays.fill(dp[i], true);

int start = -1; // 最长回文子串的起始索引
int end = -1; // 最长回文子串的结束索引
int maxLen = -1; // 最长回文子串的长度

// 倒序遍历字符串
for (int i = len; i >= 1; i--) {
for (int j = i; j <= len; j++) {
// 判断子串s[i-1...j-1]是否为回文串
dp[i][j] = dp[i + 1][j - 1] && (s.charAt(i - 1) == s.charAt(j - 1));
if (dp[i][j]) {
// 如果是回文串,且长度大于当前最长回文子串长度,更新start, end, 和 maxLen
if (j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i - 1;
end = j;
}
}
}
}

return s.substring(start, end); // 返回最长回文子串
}
}

状态定义:

  • 用一个二维布尔数组 dp[i][j] 来表示从第 i 个字符到第 j 个字符是否是回文子串。
  • dp[i][j]true 表示 s[i-1...j-1] 是回文子串,false 则表示不是。

初始化:

  • 所有 dp[i][i] 都是 true,因为单个字符是回文。
  • 所有 dp[i][i-n] 也是被设置为true,这是根据状态转移方程需要而设置的。

状态转移方程:

  • dp[i][j] = dp[i+1][j-1] && (s.charAt(i-1) == s.charAt(j-1))

  • 这意味着如果 s[i-1] 等于 s[j-1],且 s[i...j] 的内部子串 s[i+1...j-1] 是回文,那么 s[i...j] 也是回文。

    注:

    • 这里s[i][...]的的计算是依赖于s[i+1][...]的,所以i倒着遍历;
    • dp[i][i+1] = dp[i+1][i] && (s.charAt(i-1) == s.charAt(j-1)) ,需要用到dp[i+1][i]这种无意义的状态,为了不影响合理的状态转移,所以将这种无意义状态设置为true
    • 初始化dp[len+2][len+2],多余的空间也是为了不对特殊情况做判断,维持代码的美观。

中心开花法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
//定义两个边界为全局变量
int left;
int right;
public String longestPalindrome(String s) {
left = 0;
right = 0;
//在遍历字符时将字符串转换为字符数组操作效率更高
char[] charArray = s.toCharArray();
for (int i = 0; i < charArray.length-1; i++) {
valid(charArray,i,i);//单个字符中心开花判断
valid(charArray,i,i+1);//两个字符中心开花
}
return new String(charArray,left,right-left+1);

}
public void valid(char[] charArray , int i ,int j){
while (i>= 0 && j < charArray.length && charArray[i] == charArray[j]){
i--;
j++;
}
//循环结束时,判断该回文子串是否为最大
i++;//结束时i,j已经不是回文子串的范围,所以需要将其加加
j--;
if (j - i > right -left){
left = i;
right = j;
}
}
}

运行速度比动态规划法要快很多。

1143. 最长公共子序列 - 力扣(LeetCode)

难度:中等

这个题的难点在于分析出状态转移方程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length(); // 获取text1的长度
int len2 = text2.length(); // 获取text2的长度
int[][] dp = new int[len1+1][len2+1]; // 定义dp数组,dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列长度
char[] charArray1 = text1.toCharArray(); // 将text1转换为字符数组
char[] charArray2 = text2.toCharArray(); // 将text2转换为字符数组

// 遍历text1和text2的每个字符
for(int i=1; i<=len1; i++) {
for(int j=1; j<=len2; j++) {
// 如果当前字符相等,则dp[i][j]等于dp[i-1][j-1]加1
if(charArray1[i-1] == charArray2[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else
// 否则,dp[i][j]等于dp[i][j-1]和dp[i-1][j]的较大值
dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
}
}
// 返回dp数组的最后一个元素,即最长公共子序列的长度
return dp[len1][len2];
}
}

定义状态

  • dp[i][j] 表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列的长度。

状态转移方程

  • 如果 text1[i-1] == text2[j-1],那么 dp[i][j] = dp[i-1][j-1] + 1
  • 如果 text1[i-1] != text2[j-1],那么 dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j])
1
2
3
4
text1 = 'abcde'  
text2 = 'ace'
dp[3][2] = dp[2][1]+1;
dp[3][3] = max(dp[3][2],dp[2][3]);

72. 编辑距离 - 力扣(LeetCode)

难度:中等

本体的难度在于抽象出动态规划的状态和状态转移方程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];

// 初始化边界条件:dp[i][0] = i 表示将 word1 的前 i 个字符删除需要 i 步
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
// 初始化边界条件:dp[0][j] = j 表示将 word2 的前 j 个字符插入需要 j 步
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}

// 将 word1 和 word2 转换为字符数组,方便按字符处理
char[] charArray1 = word1.toCharArray();
char[] charArray2 = word2.toCharArray();

// 动态规划填表过程
for (int i = 1; i <= m; i++) {
char char1 = charArray1[i - 1];
for (int j = 1; j <= n; j++) {
char char2 = charArray2[j - 1];

// 在word2中插入:dp[i][j-1] + 1,对应将 word1 的前 i 个字符转换为 word2 的前 j-1 个字符
int insert_j = dp[i][j - 1] + 1;

// 在word1中插入:dp[i-1][j] + 1,对应将 word1 的前 i-1 个字符转换为 word2 的前 j 个字符
int insert_i = dp[i - 1][j] + 1;

// 替换操作:dp[i-1][j-1] + 1(如果 char1 != char2),否则 dp[i-1][j-1]
int replace = dp[i - 1][j - 1];
if (char1 != char2) {
replace += 1;
}

// 取三者中的最小值
dp[i][j] = Math.min(Math.min(insert_j, insert_i), replace);
}
}

// 返回最终结果:将 word1 转换为 word2 所需的最小操作数
return dp[m][n];
}
}

初始化

  • dp[i][0] = i:将 word1 的前 i 个字符删除需要 i 步。
  • dp[0][j] = j:将 word2 的前 j 个字符插入需要 j 步。

状态转移

  • word2中插入dp[i][j-1] + 1 对应在 word2 中插入一个字符,使得前 i 个字符能与 word2 的前 j 个字符匹配。
  • word1中插入dp[i-1][j] + 1 对应在 word1 中删除一个字符,使得前 i-1 个字符能与 word2 的前 j 个字符匹配。
  • 替换操作dp[i-1][j-1] + 1(如果当前字符不相等)或者 dp[i-1][j-1](如果当前字符相等)。

动态规划

  • 利用三种操作的最小值更新 dp[i][j],从而得到将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数。

结果

  • 最后返回 dp[m][n],即将整个 word1 转换为 word2 所需的最小操作数。