服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

彻底理解动态规划:编辑距离

日期: 来源:码农的荒岛求生收集编辑:码农的荒岛求生

大家好,我是小风哥。

这是动态规划主题的第三篇本篇的题目非常经典,几乎是面试必备,即,编辑距离问题,edit distance;

给定两个字符串word1以及word2,返回将word1转为word2需要的最少步骤,在每一步中你可以针对字符串word1进行以下操作:

  • 新增一个字符
  • 删除一个字符
  • 替换一个字符

假如word1是"horse",word2是“ros”,那么你的程序需要返回3,也就是说将word1转为word2至少需要三个步骤:

  1. 将word1中的第一个字符h替换为字符r:horse -> rorse,此时word1变为rorse,word1与word2前两个字符相等
  2. 将word1中的第三个字符r删掉:rorse -> rose,此时word1变为rose,word1与word2的前三个字符相等
  3. 将word1中的最后一个字符删掉:rose -> ros,此时word1与word2相等。

想一想该怎样用动态规划解决这个问题。


选择与子问题

和之前的题目一样,你首先应该找出子问题是什么,子问题与原始问题的依赖关系是什么。

找出子问题的关键在于每一步的选择。

如果word1与word2的第一个字符相等,假设word1是hor、word2是hr,那么我们可以放心的排除掉两个字符串的第一个字符,即EditDistance("hor", "hr")一定等于EditDistance("or", "r"):

此时我们得到了一个子问题EditDistance("or", "r"),原始问题EditDistance("hor", "hr")的值等于该子问题。

真正有趣的是如果word1与word2的第一个字符不相等的情况,假设word1为“hor”,而word2为“ro”,此时根据该问题的规则针对word1的第一个字符有三种操作:

1,在word1的第一个字符前新增(Insert)一个字符r,此时word1变为rhor,由于此时word1 的第一个字符等于word2的第一个字符,可以放心的忽略掉,因此我们得到了子问题EditDistance("hor","o"),由于执行了一次新增操作,因此:

EditDistance("hor","ro") = EditDistance("hor","o") + 1

2,将word1的第一个字符删掉(Delete),此时word1变为“or”,我们得到了一个新的子问题EditDistance("or","ro"),由于执行了一次删除操作,因此:

EditDistance("hor","ro") = EditDistance("or","ro") + 1

3,将word1的第一个字符替换(Replace )为r,此时word1变为了“ror”,由于word1的第一个字符等于word2的第一个字符,因此可以放心的忽略掉,我们得到了一个新的子问题EditDistance("or","o"),由于执行了一次删除操作,因此:

EditDistance("hor","ro") = EditDistance("or","o") + 1

根据题目要求,我们需要得到最小的编辑距离,因此:

EditDistance("hor","ro") = min(EditDistance("hor","o"),
                               EditDistance("or","ro"),
                               EditDistance("or","o")) + 1

即:

可以看到,如果word1与word2的第一个字符如果不相等的话那么我们会得到三个子问题,取这三个子问题的最小值然后加1就是原始问题的解。

现在我们找到了子问题与原始问题之间的依赖关系。

实际上,根据上述讨论我们还可以进一步扩展从而得到完整的状态空间树。

从这棵树中可以看到最小的编辑距离是2。

现在你应该清楚的知道该怎样我们是怎样一步步将问题不断的分解为更小的子问题,然后利用子问题的解来得到原始问题的解了。


自顶向下递归代码

上图中每个方框都是一个子问题,决定一个子问题的因素在于word1与word2当前处理到了哪个位置,假设对word1处理到了第i个位置,对word2处理到了第j个位置,因此我们可以对问题进行定义:

int EditDistance(int i, int j);

该函数表示从i到word1的末尾形成的字符串与从j从word2的末尾形成的字符串的编辑距离。

因此如果调用该函数时我们应该这样使用:

EditDistance(0, 0);

有了该定义与上述分析,你可以轻而易举的写出这样的递归代码:

string word1;
string word2;

int EditDistance(int i, int j) {
  if (i == word1.length() && j == word2.length()) return 0;
  if (i == word1.length()) return word2.length() - j;
  if (j == word2.length()) return word1.length() - i;

  if (word1[i] == word2[j]) return EditDistance(i + 1, j + 1);
  else {
    return min(EditDistance(i + 1, j + 1), min(
               EditDistance(i, j + 1),
               EditDistance(i + 1, j))) + 1;
  }
}

我们将word1与word2声明为全局变量,这样你可以清楚的看到决定EditDistance函数值的因素只有这两个参数i和j,i的取值为[0, word1.length()],j的取值为[0, word2.length()],也就是说子问题的个数只有(word1.length() + 1) * (word2.length() + 1) 个,上述递归代码存在大量重复计算问题,因此可以通过增加cache进行优化,这个改动就留给大家啦。

接下来我们着手将自顶向下的递归代码改为自底向上的动态规划代码。


自底向上动态规划代码

由于子问题的个数只有(word1.length() + 1) * (word2.length() + 1) 个,因此可以定义一个相同大小的二维数组dp:

vector<vector<int>>dp(word1.length() + 1, vector<int>(word2.length() + 1, 0));

接下来我们要求解最小子问题,最小子问题就是上述递归代码的递归出口:

if (i == word1.length() && j == word2.length()) return 0;

该最小子问题的解包含在了dp数组的初始化中。

接下来的子问题是另外两个递归出口:

if (i == word1.length()) return word2.length() - j;
if (j == word2.length()) return word1.length() - i;

我们可以简单的构造出两种情况下的所有i和j来初始化数组dp,即:

for (int j = word2.length() - 1; j >= 0; j--)
  dp[word1.length()][j] = word2.length() - j;
for (int i = word1.length() - 1; i >= 0; i--)
  dp[i][word2.length()] = word1.length() - i;

最后我们利用两个for循环来构造出所有的i和j,从而将递归函数的最后一部分:

if (word1[i] == word2[j]) return EditDistance(i + 1, j + 1);
else {
  return min(EditDistance(i + 1, j + 1), min(
             EditDistance(i, j + 1),
             EditDistance(i + 1, j))) + 1;
}

放置在for循环中,并将对递归函数的调用替换为对数组dp的读写:

for (int i = word1.length() - 1; i >= 0; i--) {
  for (int j = word2.length() - 1; j >= 0; j--) {
    if (word1[i] == word2[j])
      dp[i][j] = dp[i + 1][j + 1];
    else
      dp[i][j] = min(dp[i + 1][j + 1], min(dp[i][j + 1], dp[i + 1][j])) + 1;
  }
}

最终,完整的动态规划代码为:

int minDistance(string word1, string word2) {
  vector<vector<int>>dp(word1.length() + 1, vector<int>(word2.length() + 1, 0));
  for (int j = word2.length() - 1; j >= 0; j--)
    dp[word1.length()][j] = word2.length() - j;
  for (int i = word1.length() - 1; i >= 0; i--)
    dp[i][word2.length()] = word1.length() - i;
  for (int i = word1.length() - 1; i >= 0; i--) {
    for (int j = word2.length() - 1; j >= 0; j--) {
      if (word1[i] == word2[j])
        dp[i][j] = dp[i + 1][j + 1];
      else
        dp[i][j] = min(dp[i + 1][j + 1], min(dp[i][j + 1], dp[i + 1][j])) + 1;
    }
  }

  return dp[0][0];
}
最后,我建了微信技术群,扫描下方二维码备注写“加群”二字即可,一起见证我们的成长。

相关阅读

  • 学会夸人

  • 我有个口头禅,熟悉的读者应该都知道,就是「很棒」。1.以前带团队,每次交互同学给过来一个方案,我看都没看就会先说:“为什么这么棒?!”看过后如果真的好,我会接着夸:“怎么能这么优秀
  • 作品集为心仪工作而做准备-私教指导

  • 我是张增顺,07年就开始做设计,在行业中已经十余年,从自我提升的前提下,从专业能力,通用能力以外利用自己的时间进行一些自己的影响力,通过自己多年的经验和沉淀为大家答疑解惑,帮助
  • 福利已到 | 抽本本

  • 2023福利“年初不放血 一年全白忙” 去年公司的生意较为惨淡,一年到头客户没增加几个不说,到年底了眼看着友商业绩夺冠,某司集体新冠......为求今年转个运,且在粉丝的呼声中,在
  • 行稳致远 | 节奏

  • 编者按看准三年翻一倍,也不能上来先深套买入当下就能涨,也不能电梯来回坐不光要深入地研究,基金经理还得理解市场的节奏才能有稳健的曲线。本文推送的心法就是至今还没羊过的张
  • 预防新冠感染重症发生!浙江印发最新通知

  • 日前,省卫生健康委办公室印发通知,要求进一步做好重点人群健康随访工作,预防和减少新冠病毒感染重症发生。>>>随访对象随访对象主要包括辖区65岁及以上老年人,重点针对前期新冠
  • 吹响拼经济号角|瓶窑,“卯”足干劲等你来!

  • 2022年,面对疫情多点散发和国内外严峻形势,瓶窑镇深入学习贯彻党的二十大精神和省、市、区党委政府有关经济稳进提质工作部署,坚持以建设杭州城市新中心、争当“两个先行”排头

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 彻底理解动态规划:编辑距离

  • 大家好,我是小风哥。这是动态规划主题的第三篇,本篇的题目非常经典,几乎是面试必备,即,编辑距离问题,edit distance;给定两个字符串word1以及word2,返回将word1转为word2需要的最少
  • 为什么计算机需要操作系统?

  • 大家好,我是小风哥,提前祝大家新年快乐,这是年前的最后一篇技术文啦。今天我们从三个方面来简单聊聊为什么计算机系统操作系统这个话题。资源分配器如果你的CPU上只需要运行一
  • 系统调用与函数调用有什么区别?

  • 大家新年好,我是小风哥,这是今年的第一篇技术文,我们来聊聊系统调用与普通的函数调用之间的区别。作为程序员你肯定写过无数的函数,假设有这样两个函数:void funcB() {}void func
  • 深入理解Linux系统调用

  • 大家好,我是小风哥。在前两篇文章《为什么计算机需要操作系统》《系统调用与函数调用有什么区别》中我们了解了什么是系统调用、为什么需要系统调用、系统调用与函数调用有什
  • 世界糖尿病日||中医话中医糖尿病“消渴病”

  • 2022.11.14世 界 糖 尿 病 日控制血糖全身健康- 人人享有糖尿病健康管理 -中医话·糖尿病西医学中的糖尿病在中医归属于“消渴病”的范畴,消渴是以多饮、多食、多尿、身体消
  • 喜讯!25项科研成果获奖,获奖数创历史新高!

  • 本文转自北京中医药大学官方公众号近日,中华中医药学会公布了2022年度中华中医药学会科学技术奖励评审结果。我校前期积极进行相关宣讲、做好组织申报工作,并召开多轮专家论证