NeFut Logo NeFut
EN 管理员登录

多序列动态规划:最长公共子序列与编辑距离的深入解析

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

多序列动态规划的本质是在多维笛卡尔积构成的状态空间中进行拓扑路径搜索。本节聚焦于最长公共子序列(LCS)与编辑距离(Edit Distance),这两类问题展示了在“对齐”与“匹配”过程中的状态演变。

1. 最长公共子序列 (LCS)

给定两个序列 $A$(长度为 $N$)和 $B$(长度为 $M$),求其最长公共子序列的长度。其核心逻辑在于判定当前末尾元素 $A[i]$ 与 $B[j]$ 的对等性:

2. 编辑距离 (Edit Distance)

给定两个字符串 $A$ 和 $B$,计算将 $A$ 转换为 $B$ 所需的最少操作次数(允许插入、删除、替换字符)。编辑距离在几何上可以看作在一张二维网格图上,从左上角 $(0,0)$ 走到右下角 $(N,M)$ 的最短路径问题。每一种操作对应了状态空间中的一个位移向量:


状态设计与算法推导

1. LCS 状态与方程

$$f[i][j] = \begin{cases} f[i-1][j-1] + 1 & \text{if } A[i] == B[j] \\ \max(f[i-1][j], f[i][j-1]) & \text{if } A[i] \neq B[j] \end{cases}$$

2. 编辑距离状态与方程

$$f[i][j] = \min \begin{cases} f[i-1][j] + 1 & \text{(删除操作)} \\ f[i][j-1] + 1 & \text{(插入操作)} \\ f[i-1][j-1] + \text{cost} & \text{(替换操作)} \end{cases}$$

其中,若 $A[i] == B[j]$ 则 $\text{cost} = 0$,否则 $\text{cost} = 1$。


C++ 标准源码(NOIP风格)

以下源码在同一程序中实现了朴素的 $O(N \times M)$ 最长公共子序列与编辑距离算法。

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;

const int MAXN = 2005; // 针对 NOIP 传统二维 DP 数据范围
int lcs_f[MAXN][MAXN];
int edit_f[MAXN][MAXN];

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    string s1, s2;
    if (!(cin >> s1 >> s2)) return 0;

    int n = s1.length();
    int m = s2.length();

    // 为了符合算法习惯,将字符串索引转换为从 1 开始
    string a = " " + s1;
    string b = " " + s2;

    // ==================== 1. 最长公共子序列 (LCS) ====================
    // 边界显式赋值(虽然全局变量默认全 0,但工程规范要求显式体现)
    for (int i = 0; i <= n; ++i) lcs_f[i][0] = 0;
    for (int j = 0; j <= m; ++j) lcs_f[0][j] = 0;

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            if (a[i] == b[j]) {
                lcs_f[i][j] = lcs_f[i - 1][j - 1] + 1; // 致命踩坑点:此处决不能与后方的 max 混淆
            } else {
                lcs_f[i][j] = max(lcs_f[i - 1][j], lcs_f[i][j - 1]);
            }
        }
    }

    // ==================== 2. 编辑距离 (Edit Distance) ====================
    // 边界初始化:空串转换的代价
    for (int i = 0; i <= n; ++i) edit_f[i][0] = i;
    for (int j = 0; j <= m; ++j) edit_f[0][j] = j;

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            int cost = (a[i] == b[j]) ? 0 : 1;

            // 三种转换向量取极小值
            int del_op = edit_f[i - 1][j] + 1;
            int ins_op = edit_f[i][j - 1] + 1;
            int rep_op = edit_f[i - 1][j - 1] + cost;

            edit_f[i][j] = min({del_op, ins_op, rep_op});
        }
    }

    cout << "LCS: " << lcs_f[n][m] << "\n";
    cout << "Edit Distance: " << edit_f[n][m] << "\n";

    return 0;
}

NOIP 实战避坑指南

1. 字符串下标与 DP 状态下标错位

2. 编辑距离中忽视 $A[i] == B[j]$ 时的替换代价


经典 NOIP/洛谷 真题

1. 洛谷 P2758 编辑距离

2. 洛谷 P1156 垃圾陷阱

  1. 用来堆叠:$f[i][j + h_i] = \max(f[i][j + h_i], f[i-1][j] - (t_i - t_{i-1}))$。若 $j + h_i \ge D$,则可以直接输出 $t_i$ 并终止程序。
  2. 用来吃:$f[i][j] = \max(f[i][j], f[i-1][j] - (t_i - t_{i-1}) + f_i)$。 如果所有垃圾遍历完仍无法让高度 $\ge D$,则全局最大存活时间即为高度为 0 状态所能支撑的最大跨度。
原文链接: local://14.2

[h] 返回首页