给你两个单词 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 
        关键点解析:
- 
动态规划定义
dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最少操作数 。 - 
边界条件:
dp[i][0] = i:将word1的前i个字符全部删除。dp[0][j] = j:向空字符串插入word2的前j个字符 。
 - 
状态转移:
- 字符相同 :直接继承左上角的值(
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])。 
 - 插入 :
 
 - 字符相同 :直接继承左上角的值(
 - 
复杂度:
- 时间复杂度: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)。 
 - 删除A[i] → 状态回退到
 
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]]。 - 斐波那契数列:仅需存储前两个状态。
 
🧩 五、动态规划的通用解题框架
按四步法系统化求解:
- 状态定义 :明确
dp[i][j]或dp[i]的含义。 - 边界条件 :初始化最小子问题的解(如
dp[0]=0)。 - 状态转移:用数学公式覆盖所有决策。
 - 计算顺序:按依赖顺序填表(自底向上)或递归+记忆化(自顶向下)。
 
自顶向下 vs 自底向上:
方式 优点 缺点 自顶向下(记忆化) 代码直观,按需计算子问题 递归栈溢出风险 自底向上(迭代) 无递归开销,空间优化更灵活 可能计算无用子问题 
💎 六、总结:吃透动态规划的关键点
- 本质理解:动态规划是"聪明地穷举",核心是复用子问题解。
 - 状态设计:从问题的最后一步逆向定义状态,确保无后效性。
 - 转移方程 :分情况讨论(匹配/不匹配),用
min/max覆盖决策。 - 边界驱动:边界是递推的锚点,需特殊处理(空串、零值等)。
 - 优化方向:滚动数组、状态压缩是高频优化手段。
 
终极检验:尝试手推背包问题(0-1背包)的状态转移表,并写出空间优化版代码。