子序列问题是字符串DP的灵魂——删除不改变顺序,插入填补空隙
子序列(Subsequence)是从原序列中删除若干元素(不改变顺序)得到的新序列。子序列DP的核心是:对于每个元素,考虑"选"还是"不选"。
| 概念 | 要求 | 连续性 |
|---|---|---|
| 子序列 Subsequence | 顺序不变 | 不要求连续 |
| 子串 Substring | 顺序不变 | 必须连续 |
| 子数组 Subarray | 顺序不变 | 必须连续(数组版) |
给定两个字符串,返回最长公共子序列的长度。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
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]
时间: O(m * n) 空间: O(m * n)
给定整数数组,返回最长严格递增子序列的长度。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails = []
for num in nums:
lo, hi = 0, len(tails)
while lo < hi:
mid = (lo + hi) // 2
if tails[mid] < num:
lo = mid + 1
else:
hi = mid
if lo == len(tails):
tails.append(num)
else:
tails[lo] = num
return len(tails)
| 方法 | 时间 | 空间 | 特点 |
|---|---|---|---|
| DP | O(n^2) | O(n) | 能求具体LIS |
| 贪心+二分 | O(n log n) | O(n) | 只能求长度 |
给定字符串s,返回最长回文子序列的长度。
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2 if length > 2 else 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
时间: O(n^2) 空间: O(n^2)
在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:
# 通用解题模板
class Solution:
def solve(self, input_data):
# 1. 边界检查
if not input_data:
return default_value
# 2. 初始化
result = initial_value
state = initial_state
# 3. 主循环
for item in input_data:
state = transition(state, item)
result = max/min/update(result, state)
# 4. 返回结果
return result
每道经典题目都有丰富的变体。掌握核心模板后,灵活应对变体才是面试的真正挑战。以下列举本课相关的高频变体和扩展思路。
# 面试沟通模板
# 1. 先说思路(1-2分钟)
"这道题我想到两种解法,先说更直观的一种..."
# 2. 分析复杂度(30秒)
"时间复杂度O(n^2),空间可以优化到O(n)"
# 3. 边写边说
"这里我定义dp[i]表示...,转移方程是..."
# 4. 主动验证
"我用示例走一遍:dp[0]=..., dp[1]=..."
# 5. 讨论优化
"空间还可以进一步优化,因为dp[i]只依赖..."
| 检查项 | 常见错误 |
|---|---|
| 初始条件 | dp[0]是否正确?空输入是否处理? |
| 遍历范围 | 循环从0还是1开始?是否越界? |
| 状态转移 | 依赖的状态是否已计算? |
| 返回值 | 是dp[n]还是max(dp)? |
| 边界情况 | 单元素、全相同、空数组 |
子序列问题有三个核心模型,它们的DP定义和遍历方式各不相同。
| 模型 | 状态定义 | 遍历方式 | 时间 | 空间 |
|---|---|---|---|---|
| LCS | dp[i][j]=前i与前j的LCS长 | 双重正序 | O(mn) | O(mn) |
| LIS(DP) | dp[i]=以i结尾的LIS长 | i正序,j逆序检查 | O(n^2) | O(n) |
| LIS(二分) | tails[i]=长度i+1的最小尾 | 单次遍历+二分 | O(nlogn) | O(n) |
| LPS | dp[i][j]=s[i..j]的LPS长 | 区间长度递增 | O(n^2) | O(n^2) |