🔤 字符串DP — 编辑距离与正则匹配

字符串DP是文本处理的核心——编辑距离是NLP的基石

📖 字符串DP概览

字符串DP的核心是:两个字符串的对齐问题。用二维DP表,行代表一个字符串,列代表另一个字符串,每个格子记录对齐到该位置的某种最优结果。

字符串DP核心模型: +---------------------------------------+ | 字符串DP问题谱系 | +---------------------------------------+ | 编辑距离: 三种操作(增删改)的最少次数 | | 正则匹配: *和.的通配符匹配 | | 通配符匹配: *和?的匹配(LC 44) | | 最长公共子串: 最长连续公共部分 | | 回文子串/子序列: 对称性DP | | 不同的子序列: 计数问题(LC 115) | +---------------------------------------+ 二维DP表通用模式: "" s2[0] s2[1] s2[2] ... "" 0 ... s1[0] ... s1[1] ...

1. 编辑距离的三种操作

插入(Insert):在s1中插入一个字符 --> dp[i][j-1] + 1
删除(Delete):从s1中删除一个字符 --> dp[i-1][j] + 1
替换(Replace):替换s1的一个字符 --> dp[i-1][j-1] + 1
匹配(Match):若字符相同则无需操作 --> dp[i-1][j-1]

🔬 题目一:编辑距离(LC 72)

给定两个字符串,返回将word1转换成word2所使用的最少操作数。

思路分析

编辑距离DP表: word1 = "horse", word2 = "ros" "" r o s "" 0 1 2 3 h 1 1 2 3 o 2 2 1 2 r 3 2 2 2 s 4 3 3 2 e 5 4 4 3 <-- 答案=3 操作: horse -> rorse(改h->r) -> rose(删r) -> ros(删e)

代码实现

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        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:
                    dp[i][j] = 1 + min(
                        dp[i-1][j],     # 删除
                        dp[i][j-1],     # 插入
                        dp[i-1][j-1]    # 替换
                    )
        return dp[m][n]

复杂度分析

时间: O(m * n) 空间: O(m * n)

编辑距离是NLP和生物信息学的基础算法。变体很多:只允许插入/删除、不同操作权重不同、只允许替换等。面试中先写标准版本,再讨论变体。

🔬 题目二:正则表达式匹配(LC 10)

给定字符串s和模式p,实现支持 .* 的正则匹配。

思路分析

正则匹配DP: 难点: * 可以匹配0次或多次 dp[i][j] = s前i个字符是否与p前j个字符匹配 情况1: p[j-1] != '*' 直接匹配: dp[i][j] = dp[i-1][j-1] and (p[j-1]==s[i-1] or p[j-1]=='.') 情况2: p[j-1] == '*' *匹配0次: dp[i][j] = dp[i][j-2] (跳过x*) *匹配1+次: dp[i][j] = dp[i-1][j] and (p[j-2]==s[i-1] or p[j-2]=='.') s[i-1]被*吃掉,但*仍然可用 初始化: dp[0][0] = True (空串匹配空模式) dp[0][j] = dp[0][j-2] 当p[j-1]=='*'

代码实现

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)
        dp = [[False] * (n + 1) for _ in range(m + 1)]
        dp[0][0] = True
        for j in range(2, n + 1):
            if p[j-1] == '*':
                dp[0][j] = dp[0][j-2]

        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if p[j-1] == '*':
                    dp[i][j] = dp[i][j-2] or \
                        (dp[i-1][j] and (p[j-2] == s[i-1] or p[j-2] == '.'))
                else:
                    dp[i][j] = dp[i-1][j-1] and \
                        (p[j-1] == s[i-1] or p[j-1] == '.')
        return dp[m][n]

复杂度分析

时间: O(m * n) 空间: O(m * n)

正则匹配是Hard题,核心难点在于*的处理。关键洞察:*永远和前一个字符绑定(x*作为一个整体),匹配0次则跳过整体,匹配多次则消耗s的一个字符但保留*。初始化时别忘了空串可以匹配x*y*z*这种模式。

🔬 题目三:最长回文子串(LC 5)

给定字符串s,返回s中最长回文子串。

思路分析

中心扩展法(推荐面试写法):
1. 每个字符和每对相邻字符都可以是回文中心
2. 从中心向两端扩展,直到不匹配为止
3. 记录最长回文的起始位置和长度
4. 时间O(n^2),空间O(1)

代码实现

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n <= 1:
            return s
        start, max_len = 0, 1

        def expand(l, r):
            nonlocal start, max_len
            while l >= 0 and r < n and s[l] == s[r]:
                if r - l + 1 > max_len:
                    start = l
                    max_len = r - l + 1
                l -= 1
                r += 1

        for i in range(n):
            expand(i, i)      # 奇数长度回文
            expand(i, i + 1)  # 偶数长度回文
        return s[start:start + max_len]

复杂度分析

时间: O(n^2) 空间: O(1)

回文问题还有Manacher算法O(n),但面试极少要求。推荐掌握中心扩展法。面试中中心扩展 > DP > Manacher(除非面试官明确要求)。

面试实战技巧

在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:

1. 解题步骤模板

5分钟分析:理解题意 - 确认边界 - 想暴力解 - 优化 - 写代码

1. 先复述题目,确认理解无误
2. 问清边界条件:空输入?负数?溢出?
3. 先说暴力解法(证明你理解问题)
4. 分析瓶颈,提出优化思路
5. 边写代码边解释思路

2. 常见陷阱

通用陷阱:
1. 索引越界:特别注意循环范围和数组边界
2. 初始化遗漏:DP的初始条件是否正确?
3. 遍历方向:依赖关系决定遍历顺序
4. 整数溢出:Python不溢出但其他语言注意
5. 边界条件:空数组、单元素、全相同

3. 代码模板

# 通用解题模板
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
面试中的代码不是越短越好,而是越清晰越好。适当的注释、合理的变量名、分步骤的实现,比一行式解法更能展示工程素养。
成就解锁:字符串匠人 — 掌握编辑距离与正则匹配两大字符串DP核心问题
LeetCode AC验证:LC 72 Edit Distance ✅ | LC 10 Regular Expression Matching ✅ | LC 5 Longest Palindromic Substring ✅

📝 课后练习

  1. LC 44 通配符匹配
  2. LC 115 不同的子序列
  3. LC 647 回文子串
  4. LC 131 分割回文串

🔑 本课要点回顾

📚 扩展阅读

思考题:编辑距离中,如果插入和删除的代价为1,替换的代价为2,DP转移方程该如何修改?

⚡ 进阶专题:常见变体与扩展

每道经典题目都有丰富的变体。掌握核心模板后,灵活应对变体才是面试的真正挑战。以下列举本课相关的高频变体和扩展思路。

1. 常见题目变体

条件变化型:在原题基础上增加约束条件,如:数组中有负数、元素有上限、需要考虑溢出等。核心思路:不改变DP框架,只在转移时增加条件判断。

维度扩展型:从一维DP扩展到二维或三维。如:从线性到环形(LC 213)、从单串到双串(LCS)、从无约束到有约束。核心思路:增加状态维度来编码额外信息。

目标变化型:从求最值变为求方案数、求具体方案、判定可行性。核心思路:dp存的值从max/min变为count/boolean,转移从取最值变为累加。

2. 面试中的沟通技巧

# 面试沟通模板
# 1. 先说思路(1-2分钟)
"这道题我想到两种解法,先说更直观的一种..."

# 2. 分析复杂度(30秒)
"时间复杂度O(n^2),空间可以优化到O(n)"

# 3. 边写边说
"这里我定义dp[i]表示...,转移方程是..."

# 4. 主动验证
"我用示例走一遍:dp[0]=..., dp[1]=..."

# 5. 讨论优化
"空间还可以进一步优化,因为dp[i]只依赖..."

3. 代码调试检查清单

检查项常见错误
初始条件dp[0]是否正确?空输入是否处理?
遍历范围循环从0还是1开始?是否越界?
状态转移依赖的状态是否已计算?
返回值是dp[n]还是max(dp)?
边界情况单元素、全相同、空数组
面试中主动验证是最被低估的技巧。用示例手动走一遍DP表,既能发现bug,又能展示你的严谨性。很多面试官就是看你能否主动发现和修正错误。