零基础理解动态规划:
动态规划就像是"填表格"。我们把一个大问题拆成很多个小问题,把小问题的答案记在表格里。当我们要算大问题时,直接查表利用之前算好的结果,这样就不用重复计算了。
1. 不同路径 (Unique Paths)
题目描述: 一个机器人位于 m×nm \times nm×n 网格的左上角。它每次只能向下或向右移动一步。问到达右下角有多少条不同的路径?
通俗思路:
- 拆解: 机器人想去
(i, j)这个格点,它只有两种可能:从上面 来(i-1, j),或者从左边 来(i, j-1)。 - 推导: 到达
(i, j)的总路径数 = 到达上面的路径数 + 到达左边的路径数。 - 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
答案代码(Python):
python
def uniquePaths(m, n):
# 初始化表格,全部填1(因为第一行和第一列只有一种走法:一直向右或一直向下)
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
# 当前格子的路径 = 上面格子的路径 + 左边格子的路径
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
2. 最小路径和 (Minimum Path Sum)
题目描述: 给定一个包含非负整数的 m×nm \times nm×n 网格,找出一条从左上角到右下角的路径,使得路径上的数字总和最小。
通俗思路:
- 拆解: 想让到达
(i, j)的路径和最小,你要看从上面下来更便宜,还是从左边过来更便宜。 - 推导: 当前格子的最小和 = 当前格子的数值 + min(上边格子的最小和, 左边格子的最小和)。
- 状态转移方程:
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
答案代码:
python
def minPathSum(grid):
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
for i in range(m):
for j in range(n):
if i == 0 and j == 0: dp[i][j] = grid[0][0] # 起点
elif i == 0: dp[i][j] = dp[i][j-1] + grid[i][j] # 第一行只能从左来
elif j == 0: dp[i][j] = dp[i-1][j] + grid[i][j] # 第一列只能从上下来
else:
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
return dp[m-1][n-1]
3. 最长回文子串 (Longest Palindromic Substring)
题目描述: 给你一个字符串 s,找到 s 中最长的回文子串(正着读反着读都一样)。
通俗思路:
- 拆解: 如果一个字符串
s[i...j]是回文,且左右两边的字符相等s[i] == s[j],那么去掉头尾的中间部分s[i+1...j-1]也必须是回文。 - 状态:
dp[i][j]表示从第i到第j个字符构成的子串是否为回文(True/False)。 - 推导:
dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1]
答案代码:
python
def longestPalindrome(s):
n = len(s)
if n < 2: return s
dp = [[False] * n for _ in range(n)]
# 每个单独的字符都是回文
for i in range(n): dp[i][i] = True
max_len = 1
start = 0
# L 是子串长度
for L in range(2, n + 1):
for i in range(n):
j = L + i - 1 # 右边界
if j >= n: break
if s[i] == s[j]:
if j - i < 3: # 长度为2或3时,只要s[i]==s[j]就是回文
dp[i][j] = True
else:
dp[i][j] = dp[i+1][j-1]
if dp[i][j] and L > max_len:
max_len = L
start = i
return s[start:start + max_len]
4. 最长公共子序列 (Longest Common Subsequence, LCS)
题目描述: 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
通俗思路:
- 拆解: 比较两个字符串的末尾字符。
- 情况1: 末尾字符相同。那这个字符一定在公共子序列里。
长度 = 1 + 前面部分的LCS。 - 情况2: 末尾字符不同。那最长公共序列要么在
text1去掉最后一个字符后,要么在text2去掉最后一个字符后。 - 状态转移:
- 如果
s1[i] == s2[j]:dp[i][j] = dp[i-1][j-1] + 1 - 否则:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 如果
答案代码:
python
def longestCommonSubsequence(text1, text2):
m, n = len(text1), len(text2)
# 多申请一行一列处理边界(空字符串情况)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
5. 编辑距离 (Edit Distance)
题目描述: 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数(插入、删除、替换)。
通俗思路:
想象你在把 word1 变成 word2。
- 如果末尾字符一样:不需要操作。
dp[i][j] = dp[i-1][j-1]。 - 如果不一样,你有三种选择:
- 插入 一个字符:
dp[i][j-1] + 1 - 删除 一个字符:
dp[i-1][j] + 1 - 替换 一个字符:
dp[i-1][j-1] + 1
- 插入 一个字符:
- 我们要选这三个操作里步数最少的。
答案代码:
python
def minDistance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 边界情况:word1 变为空字符串,或空字符串变为 word2
for i in range(m + 1): dp[i][0] = i
for j in range(n + 1): dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] # 字符相等,不增加操作
else:
# 取 替换、删除、插入 三者中的最小值 + 1
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
return dp[m][n]
总结:做多维 DP 的三步走
- 定义表格含义:
dp[i][j]代表什么?(例如:到达(i,j)的方法数、最长长度、最小代价)。 - 找递推关系: 到达当前这一步,前一步是从哪里来的?(上边?左边?还是斜角?)。
- 定初始值: 表格的第一行和第一列(最简单的情况)该填什么。