学习笔记二

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

以下是使用 JavaScript 实现的编辑距离(Levenshtein 距离)动态规划解法,用于计算将 word1 转换成 word2 的最少操作数(插入、删除、替换):

javascript 复制代码
function minDistance(word1, word2) {
    const m = word1.length, n = word2.length;
    // 创建 (m+1) x (n+1) 的 DP 矩阵
    const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
    
    // 初始化边界条件 
    for (let i = 0; i <= m; i++) dp[i][0] = i;  // 删除所有字符
    for (let j = 0; j <= n; j++) dp[0][j] = j;  // 插入所有字符

    // 填充 DP 表 
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (word1[i - 1] === word2[j - 1]) {
                // 字符相同,无需操作 
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 取三种操作的最小值 +1 
                dp[i][j] = 1 + Math.min(
                    dp[i][j - 1],     // 插入操作
                    dp[i - 1][j],     // 删除操作
                    dp[i - 1][j - 1] // 替换操作
                );
            }
        }
    }
    return dp[m][n]; // 返回最终结果
}

// 测试用例
console.log(minDistance("horse", "ros"));      // 输出 3 
console.log(minDistance("intention", "execution")); // 输出 5 

关键点解析:

  1. 动态规划定义
    dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数 。

  2. 边界条件

    • dp[i][0] = i:将 word1 的前 i 个字符全部删除。
    • dp[0][j] = j:向空字符串插入 word2 的前 j 个字符 。
  3. 状态转移

    • 字符相同 :直接继承左上角的值(dp[i][j] = dp[i-1][j-1])。
    • 字符不同 :取以下三种操作的最小值加 1:
      • 插入dp[i][j-1] + 1(在 word1 中插入 word2[j])。
      • 删除dp[i-1][j] + 1(删除 word1[i])。
      • 替换dp[i-1][j-1] + 1(将 word1[i] 替换为 word2[j])。
  4. 复杂度

    • 时间复杂度:O(mn),需遍历整个 DP 表。
    • 空间复杂度:O(mn),需存储二维数组 。

优化建议:

  • 空间优化:可使用滚动数组(两个一维数组)将空间复杂度降至 O(min(m,n)) 。
  • 实际应用:此算法广泛用于拼写纠错、生物信息学(DNA序列比对)等场景 。

以下我将从动态规划的本质、核心要素、设计步骤到经典案例,结合逻辑推演与可视化分析,围绕状态定义→边界条件→状态转移→初始值设计的主线展开,辅以编辑距离(Levenshtein距离)的全程推演。


🧠 一、动态规划的本质与核心思想

1. 定义与核心思想

动态规划(Dynamic Programming, DP)是一种通过将复杂问题分解为重叠子问题 ,并存储子问题的解以避免重复计算,从而高效求解最优解的方法。其核心是 "记录并复用"子问题的解,本质是空间换时间。

类比:登山者记录每个休息站的最佳路径,避免重复探索(无后效性)。

2. 适用问题的三大特征

  • 最优子结构:问题的最优解包含子问题的最优解(如最短路径的子路径必最短)。
  • 重叠子问题 :子问题被重复计算(如斐波那契数列中F(3)被多次调用)。
  • 无后效性:当前状态一旦确定,后续决策不受之前状态影响(如背包问题中当前容量下的选择与之前物品选择无关)。

3. 与分治法的关键区别

  • 分治法:子问题独立(如归并排序)。
  • 动态规划:子问题重叠且依赖(如斐波那契数列)。

⚙️ 二、动态规划的四大基石

1. 状态定义:问题的数学建模

状态是子问题的抽象表示,需满足两个条件:

  • 完整描述 :涵盖子问题的所有关键变量(如编辑距离中的dp[i][j]表示字符串A前i个字符转成B前j个字符的最小操作数)。
  • 无后效性 :状态仅由当前决策决定,与历史路径无关(如背包问题中dp[i][j]只依赖前一行状态)。

设计技巧:从问题的最后一步逆向思考(如编辑距离的最后一次操作是插入、删除或替换)。

2. 边界条件:递推的起点

边界是状态转移的初始值,通常对应最小子问题的解:

  • 空字符串处理 :若A为空,转成B需插入所有字符(dp[0][j] = j)。
  • 零值初始化dp[0][0] = 0(空字符串转空字符串无需操作)。

常见边界

  • 序列问题:空序列长度为0。
  • 路径问题:起点到起点距离为0。

3. 状态转移方程:决策逻辑的数学化

方程描述状态间的递推关系,需覆盖所有决策可能:

math 复制代码
dp[i][j] = 
\begin{cases} 
dp[i-1][j-1] & \text{if } A[i] = B[j] \\
\min \begin{cases}
dp[i-1][j] + 1 & \text{(删除A[i])} \\
dp[i][j-1] + 1 & \text{(插入B[j])} \\
dp[i-1][j-1] + 1 & \text{(替换字符)}
\end{cases} & \text{otherwise}
\end{cases}

逻辑解析

  • 字符匹配:无需操作,继承左上状态。
  • 字符不匹配 :取三种操作的最小代价:
    • 删除A[i] → 状态回退到(i-1, j)
    • 插入B[j] → 状态推进到(i, j-1)
    • 替换字符 → 状态回退到(i-1, j-1)

4. 计算顺序:确保子问题已求解

必须按依赖顺序计算(如编辑距离中需先计算dp[i-1][j-1]再算dp[i][j]),通常使用自底向上填表法 (从i=0,j=0逐步迭代)。


📊 三、案例全程推演:编辑距离算法(Levenshtein距离)

A="horse"B="ros"为例,展示动态规划的全过程:

1. 初始化边界条件

0 r o s
0 0 1 2 3
h 1
o 2
r 3
s 4
e 5

2. 状态转移推演(关键步骤)

  • i=1, j=1(h→r) :字符不匹配,取左、上、左上最小值min(1+1, 1+1, 0+1)=1(替换操作)。
  • i=2, j=1(o→r) :不匹配,min(2, 1+1, 1+1)=2(删除'o')。
  • i=2, j=2(o→o) :匹配,继承左上值dp[1][1]=1

3. 最终DP表

0 r o s
0 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

结果dp[5][3]=3(删除'h'、删除'o'、替换'e'为's')。


⚡️ 四、优化技巧:降低空间复杂度

1. 滚动数组法

若状态仅依赖上一行(如编辑距离),可将二维DP压缩为两个一维数组:

python 复制代码
prev = [0, 1, 2, 3]  # 上一行
curr = [1, 0, 0, 0]   # 当前行
for i in range(1, m+1):
    curr[0] = i  # 每行首元素初始化
    for j in range(1, n+1):
        curr[j] = prev[j-1] if A[i-1]==B[j-1] else min(prev[j], curr[j-1], prev[j-1]) + 1
    prev = curr.copy()  # 滚动更新

空间复杂度 :从O(mn)降至O(n)

2. 状态压缩的适用场景

  • 背包问题:仅依赖前一行的dp[j]dp[j-w[i]]
  • 斐波那契数列:仅需存储前两个状态。

🧩 五、动态规划的通用解题框架

按四步法系统化求解:

  1. 状态定义 :明确dp[i][j]dp[i]的含义。
  2. 边界条件 :初始化最小子问题的解(如dp[0]=0)。
  3. 状态转移:用数学公式覆盖所有决策。
  4. 计算顺序:按依赖顺序填表(自底向上)或递归+记忆化(自顶向下)。

自顶向下 vs 自底向上

方式 优点 缺点
自顶向下(记忆化) 代码直观,按需计算子问题 递归栈溢出风险
自底向上(迭代) 无递归开销,空间优化更灵活 可能计算无用子问题

💎 六、总结:吃透动态规划的关键点

  1. 本质理解:动态规划是"聪明地穷举",核心是复用子问题解。
  2. 状态设计:从问题的最后一步逆向定义状态,确保无后效性。
  3. 转移方程 :分情况讨论(匹配/不匹配),用min/max覆盖决策。
  4. 边界驱动:边界是递推的锚点,需特殊处理(空串、零值等)。
  5. 优化方向:滚动数组、状态压缩是高频优化手段。

终极检验:尝试手推背包问题(0-1背包)的状态转移表,并写出空间优化版代码。

相关推荐
崔庆才丨静觅12 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax