滚动数组必须用prev和curr两个vector,因状态转移依赖dpi-1、dpi、dpi-1,单数组会覆盖左上角值;需防越界、UTF-8拆字、BOM等陷阱,空间优化至O(min(m,n))。

直接用二维 std::vector<:vector>></:vector> 算 Levenshtein 距离,在字符串长度超 10⁴ 时大概率触发 std::bad_alloc 或栈溢出——这不是算法错,是空间冗余导致的工程失败。
为什么滚动数组必须用两行 vector 而不是单数组
因为状态转移依赖三个方向:dp[i-1][j](删)、dp[i][j-1](插)、dp[i-1][j-1](换/跳过)。单数组在更新 dp[j] 时,dp[j-1] 已被覆盖,dp[i-1][j-1] 就丢了。两行滚动能同时保全“上一行完整值”和“当前行左侧未更新值”。
- 必须用
prev和curr两个独立std::vector<int></int>,不能只靠一个 vector + 临时变量模拟(除非你手动缓存左上角值) - 常见错误:写成
dp[j] = min({dp[j] + 1, dp[j-1] + 1, prev_diag + cost})却没在每次迭代前保存正确的prev_diag—— 它必须是上一轮的dp[j-1],不是当前轮的 - 正确初始化:
prev[j] = j(空串变s2.substr(0,j)),curr[0] = i(s1.substr(0,i)变空串)
如何避免 std::string 越界和字符编码陷阱
std::string::operator[] 不检查边界,s1[i] 在 i == s1.length() 时读到的是未定义字节,DP 初始条件全崩;更糟的是 UTF-8 中文被拆成多个字节比较,s1[i-1] == s2[j-1] 永远为 false。
- 字符访问一律用
s1[i-1]和s2[j-1],循环从i = 1开始,严格对应 DP 状态定义 - 调试阶段把
[]换成.at(),可捕获std::out_of_range - 若输入含中文、emoji,必须先转
std::u32string:用std::from_bytes(C++23)或std::codecvt_utf8<char32_t></char32_t>,再对char32_t数组跑 DP - 别忽略 BOM、零宽空格等不可见字符——距离为 0 但字符串不等?先
std::erase_if(s, [](char32_t c) { return c
传参和内存复用的关键细节
高频调用场景(如日志聚类、拼写纠错)下,构造/析构 std::vector 和拷贝 std::string 是主要开销来源,不是 DP 循环本身。
立即学习“C++免费学习笔记(深入)”;
- 函数签名必须是
int levenshtein(const std::string& s1, const std::string& s2),传值会触发深拷贝 - 提前交换输入顺序:
if (s1.size() > s2.size()) return levenshtein(s2, s1);,确保内层循环次数最少,空间固定为O(min(m,n)) - 生产环境别在函数内反复 new/delete vector:把
prev和curr提到类成员或静态局部变量中,用.assign(n+1, 0)复位 - 编译加
-O2,滚动数组让缓存局部性提升,10k 字符串实测比二维表快 3× 以上,内存从 ~100MB 降到 ~40KB
真正卡住人的从来不是 DP 公式本身,而是 s1[i-1] == s2[j-1] 这一行里隐藏的越界风险、UTF-8 解码偏差、以及空串判别遗漏——这些点漏掉一个,结果就不可信。


















