- 先实践
- 0-1背包问题
对于一组物品,重量不同,不可分割,需要选择一些放入背包,在不超过背包最大重量前提下,求这个最大值?
先用回溯算法实现一遍:
#include <iostream>
using namespace std;
int maxW = 0;
int n = 5; //物品个数
int weight[] = {2, 2, 4, 6, 3}; //物品重量
int w = 9; //背包承受的最大重量
void f(int i, int cw) //i 表示第i个物品 cw 表示当前背包中物品的重量
{
if(i == n || cw == w) //cw==w 表示装满了 i==n 表示考察完了
{
if(cw > maxW)
maxW = cw;
return;
}
f(i+1, cw); //选择不装第 i 个物品
if(cw + weight[i] <= w)
f(i+1, cw+weight[i]); //选择装第 i 个物品
}
int main()
{
f(0,0);
cout << maxW << endl;
return 0;
}
如果觉得规律不好找,可以用递归树将回溯算法的求解过程画出来:
递归树的每个节点表示一种状态,用(i, cw)表示。如:(2, 2) 表示我们将要决策第2个物品是否装入背包,在决策前,背包中物品重量是 2。另外,在递归树中,有些子问题的求解是重复的,如f(2, 2) 和 f(3, 4) 都被重复计算两次。这里我们可以使用备忘录,记录已经计算好的f(i, cw),当再次计算f(i, cw),可以直接从备忘录中取出来,这样可以避免冗余计算。
bool mem[5][10] = {false}; //备忘录,默认false
void f2(int i, int cw)
{
if(i == n || cw == w)
{
if(cw > maxW)
maxW = cw;
return;
}
if(mem[i][cw])
return;
mem[i][cw] = true;
f2(i+1, cw);
if(cw + weight[i] <= w)
f2(i+1, cw+weight[i]);
}
实际上,优化后的代码,它跟动态规划的执行效率基本上没差别。接下来,我们看看动态规划是怎么做的。
首先,我们把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。决策完之后,背包中物品重量会有多种情况,也就是说,会达到多种不同状态,对应到递归树中,就是不同节点。
其次,把每一层重复状态(节点)合并,只记录不同状态,再基于上一层的状态集合,来推导下一层的状态集合。可以通过合并每一层重复状态,就能保证每一层状态的个数不超过 w 个,就能成功避免每层状态个数的指数级增长。我们用一个二维数组states[n][w+1],来记录每层可以达到的不同状态。
第0个(下标从0开始编号)物品重量是2,要么装入,要么不装入背包,决策完后会有两种状态,背包中物品重量是0或2.即states[0][0] = true 和 states[0][2] = true。
第1个物品重量也是2,基于之前背包状态,在这个物品决策完之后,有3个不同状态,0(0+0),2(0+2 or 2+0),4(2+2)。即states[1][0]=true,states[1][2]=true,states[1][4]=true。
以此类推,直到考察完所有物品。整个states状态数组就计算好了。这里用图画了出来。
int knapsack()
{
bool states[5][10] = {false};
states[0][0] = true;
if(weight[0] <= w)
states[0][weight[0]] = true;
for(int i=1; i<n; i++)
{
for(int j=0; j<=w; j++) //不把第i个物品放入背包
if(states[i-1][j] == true)
states[i][j] = true;
for(int j=0; j<=w-weight[i]; j++) //把第i个物品放入背包
if(states[i-1][j] == true)
states[i][j+weight[i]] = true;
}
for(int i=w; i>=0; i--) //输出结果
if(states[n-1][i] == true)
return i;
return 0;
}
从上面代码可以轻松得到,动态规划的时间复杂度 O(n*w),回溯算法代码的时间复杂度是O(2^n),所以动态规划的执行效率高很多。但是我们需要额外申请 n乘以w+1 的二维数组,空间消耗较多。有时候我们可以说,动态规划是一种空间换时间的解决思路。这里,其实我们还可以降低空间消耗,只需要一个 w+1 的一位数组姐可以解决问题。动态规划状态转移的过程,都可以基于一位数组操作。
int knapsack2()
{
bool states[10] = {false};
states[0] = true;
if(weight[0] <= w)
states[weight[0]] = true;
for(int i=1; i<n; i++)
for(int j=w-weight[i]; j>=0; j--)
if(states[j] == true)
states[j+weight[i]] = true;
for(int i=w; i>=0; i--)
if(states[i] = true)
return i;
return 0;
}
切记,如果 j 从小到大处理,会出现for循环重复计算问题。
2. 0-1背包问题升级版
在上述题干的基础上引入物品价值的变量,在满足不超过背包最大重量的条件,是背包内物品总价值最大化。
首先来试试回溯算法解决问题:
int n = 5;
int weight[] = {2, 2, 4, 6, 3};
int w = 9;
int maxV = 0;
int value[] = {3, 4, 8, 9,6};
void f3(int i, int cw, int cv)
{
if(i == n || cw == w)
{
if(cv > maxV)
maxV = cv;
return;
}
f3(i+1, cw, cv);
if(cw + weight[i] <= w)
f3(i+1, cw+weight[i], cv+value[i]);
}
画出递归树,每个节点表示一个状态,用3个变量(i, cw, cv)表示一个状态,从下图可以看出,有几个节点的 i 和 cw是完全相同的,比如分f(2, 2, 4) 和 f(2, 2, 3),在背包总重量一样的情况,选择更大的总价值,即状态f(2, 2, 4)。即对于(i, cw)相同的不同状态,保留cv值更大的那个状态,继续递归处理,不考虑其他状态。
我们来看看动态规划怎么解决这种问题?将整个求解过程分为 n 个阶段,用一个二维数组来表示 states[n][w+1] 来记录每层可以达到的不同状态,不过这里数组存储的值是当前状态的最大价值。把每一层 (i, cw) 重复的节点合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层状态。
int knapsack3()
{
int states[5][10] = {-1};
states[0][0] = 0;
if(weight[0] <= w)
states[0][weight[0]] = value[0];
for(int i=1; i<n; i++)
{
for(int j=0; j<= w; j++) //不选择第 i 个物品
if(states[i-1][j] >= 0)
states[i][j] = states[i-1][j];
for(int j=0; j<= w-weight[i]; j++) //选择第 i 个物品
{
if(states[i-1][j] >= 0)
{
int v = states[i-1][j] + value[i];
if(v > states[i][j+weight[i]])
states[i][j+weight[i]] = v;
}
}
}
int maxvalue = -1;
for(int j=0; j<= w; j++) //找出最大值
if(states[n-1][j] > maxvalue)
maxvalue = states[n-1][j];
return maxvalue;
}
3. 双十一活动 购物
我们再来看一下生活中的例子,淘宝“双十一”,某促销活动满200减50,购物车里有n(n>100)个商品,在凑够满减条件的前提下,选出来的商品总价格最大程度地接近满减条件(200元),可以极大程度地“薅羊毛”。怎样动态规划来解决?
可以先考虑回溯算法,穷举所有的排列组合,然后看大于等于200并且最接近200的组合是哪个?但是这样效率很低,时间复杂度是指数级的。当n很大时,可能“双十一”结束了,你的程序还没跑完。实际上,这个问题跟0-1背包问题很类似,只不过把重量换成价格。购物车中n个商品,每个商品都决策是否购买,用二维数组states[n][x]记录,每次决策之后可达的状态集合。
0-1背包问题中,我们要找的是,小于等于w的最大值,这里我们找的是大于等于200中的最小值,所以 x 就不能设置200+1。如果要购买的商品总价格超过200太多,比如说1000, 就没用“薅羊毛”的实际意义了。我们可以设置x为1001。另外,我们不仅要求这个最小的价格,还要列出购买的商品是那些,可以用states数组倒推出,被选择商品的序列。好了,上代码:
void double11_shopping(int[] items, int n, int w) items 价格 n 个数 w 满减条件
{
bool **states = new bool* [n];
for(int i=0; i<n; i++)
states[i] = new bool[3*w+1];
states[0][0] = true;
if(items[0] <= 3*w)
states[0][items[0]] = true;
for(int i=0; i<n; i++)
{
for(int j=0; j<=3*w; j++) //不购买第i个商品
if(states[i-1][j] == true)
states[i][j] = true;
for(int j=0; j<=3*w; j++) //不购买第i个商品
if(states[i-1][j] == true)
states[i][j+items[i]] = true;
}
int j;
for(j=w; j<3*w+1; j++) //输出结果大于等于w的最小值
if(states[n-1][j] == true)
break;
if(j = 3*w+1) //没有可行解
return;
for(int i=n-1; i>=1; i--)
if(j-items[i] >= 0 && states[i-1][j-items[i]] == true)
{
cout << items[i] << " " << endl; //购买这个商品
j = j - items[i];
} //else 没有购买这个商品, j不变
if(j != 0)
cout << items[0] << endl;
for(int i=0; i<n; i++)
delete[] states[i];
delete[] states;
}
状态(i, j),只能从状态(i-1, j)或者(i-1, j-value[i])推导过来,我们就检查这两个状态是否可达,即是否为true。如果状态(i-1
, j)可达,说明没有购买该商品;如果(i-1, j-value[i])可达,说明该商品购买了。
- 理论
通过上面的实践练习,我们对动态规划有了初步的认识,接下来深入理解动态规划的理论知识点。
开门见山,先将它的理论简要概括为,“一个模型三个特征”。
“一个模型”,指的是动态规划适合解决什么模型,这个模型,我们称之为“多阶段决策最优解”模型。
我们一般是用动态规划解决最优问题,而过程中需要经过多个决策阶段,每个决策阶段对应一组状态。需要找出这样一组状态的决策序列,能够得到最终期望求解的最优解。
“三个特征”,分别是最优子结构、无后效性、重复子问题。
最优子结构:问题的最优解包含子问题的最优解,即通过子问题的最优解推导出问题的最优解,或者说后面阶段的状态可以通过前面阶段的状态推导出来。
无后效性:第一层含义,推到后面阶段状态的时候,只关心前面阶段的状态值,而不关心这个值是怎么推导出来的;第二层含义,某阶段状态一旦确定,就不受之后阶段决策的影响。其实,只要满足前面提到的动态规划问题模型,就基本都会满足无后效性。
重复子问题:不同的决策序列,到达相同的阶段时,可能产生重复状态。
实例剖析:
假设有一个 n * n 的矩阵w[n][n]。棋子其实位置在左上角,终点位置在右下角,每次只能向右或者向下移动一位,这样会经过很多不同的路径,把每条路径经过的数字加起来看作路径的长度。来计算一下最短路径长度?
从(0, 0) 走到 (i-1, i-1),总共要走2*(n-1)步,也就对应着2*(n-1)个阶段,每个阶段都有向下或者向右走两种决策,每个阶段都会对应一个状态集合。把状态定义为min_dst(i, j),min_dst的值表示从(0, 0) 到 (i, j) 的最短路径长度,所以这个问题符合多阶段决策最优解模型。
接下来看看是否符合“三个特征”?
首先,我们这里可以用回溯算法解决这个问题。可以自己写一下代码,画一下递归树,发现递归树中有重复的节点。这里重复的节点表示,从左上角到节点对应的位置,有多种路线,这也能说明问题中存在重复子问题。
其次,我们这里走到位置(i, j),只能通过(i-1, j),(i, j-1)两个位置移动过来,也就是说,我们想要计算位置(i, j)的状态,只需要关心(i-1, j) 和 (i, j-1)两个位置对应的状态,不关心是通过什么样的路线到达这个位置。而且,只允许往下或者向右移动,不允许后退,所以前面状态确定以后,不会被后面阶段的决策所改变,这个问题符合“无后效性”特征。
最后,我们知道到达位置(i, j),要么经过(i, j-1),要么经过(i-1, j),而且到达(i, j)的最短路径必然包含到达这两个位置的最短路径之一。换句话说,min_dist(i, j)可以通过min_dst(i, j-1) 和min_dst(i-1, j)两个状态推导过来。这个问题符合“最优子结构”特征。
min_dst(i, j) = w[i][j] + min(min_dst(i-1, j), min_dst(i, j-1));
“一个模型三个特征”已经讲完,接下来看看解决动态规划问题两种思路:状态转移表法和状态转移方程法。
状态转移表法:
我们画一个二维状态表,表中数值表示从起点到这个位置的最短路径,按照决策过程,不断状态递推演进,将表填好。弄懂了填表过程,代码实现那就简单了。
注:这里要根据上面的初始表,作累加值。
int n = 4;
int matrix[4][4] = {{1,3,5,9}, {2,1,3,4}, {5,2,6,7}, {6,8,4,3}};
int mindst_DP()
{
int states[4][4] = {0};
int sum = 0;
for(int j=0; j<n; j++) //初始化states的第一行数据
{
sum += matrix[0][j];
states[0][j] = sum;
}
sum = 0;
for(int i=0; i<n; i++) //初始化states的第一列数据
{
sum += matrix[i][0];
states[i][0] = sum;
}
for(int i=0; i<n; i++)
for(int j=0; j<n;j++)
states[i][j] = matrix[i][j] + ( states[i][j-1] > states[i-1][j] ? states[i-1][j] : states[i][j-1] );
return states[n-1][n-1];
}
状态转移方程法:
比较类似于递归的解题思路,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构。如何根据最优子结构,写出递推公式,也就是所谓的状态转移方程。一般情况下,有两种代码实现方法:递推公式加“备忘录”和迭代递推。上面例子状态转移方程已经列出来,这里再写一遍,方便查看。
min_dst(i, j) = w[i][j] + min(min_dst(i-1, j), min_dst(i, j-1));
状态转移方程是解决动态规划的关键,如果能写出状态转移方程,那动态规划就解决了一大半,翻译代码也很简单了。而很多动态规划问题的状态本身就很难定义,状态转移方程就更不好想到。下面是递归加“备忘录”的方式的代码,另一种实现方式,跟状态转移表法的代码实现一样,只是思路不同。
int matrix[4][4] = {{1,3,5,9}, {2,1,3,4}, {5,2,6,7}, {6,8,4,3}};
int n = 4;
int mem[4][4] = {0};
int min_Dist(int i, int j)
{
if(i=0 && j==0)
return matrix[0][0];
if(mem[i][j] > 0)
return mem[i][j];
int minleft=0, minup=0;
if(j-1 >= 0)
minleft = min_Dist(i, j-1);
if(i-1 >= 0)
minup = min_Dist(i-1, j);
int curMinDist = matrix[i][j] + minleft>minup ? minup : minleft;
mem[i][j] = curMinDist;
return curMinDist;
}
两种动态规划解题思路讲完了,这里强调一点,不是每个问题都同时适合两解题思路,我们要因题而异地去选择某一种解题思路。
- 再实战
搜索引擎在用户体验方面的优化有很多,其中就有经常会用到的拼写纠错功能。当你在搜索框中,一不小心输错单词时,搜索引擎会检测出你的拼写错误,并且用对应的正确单词来进行搜索。这个功能该如何实现?
1. 量化两个字符串的相似度
编辑距离:指的就是将一个字符串转化成另一个字符串,需要的最少编辑操作次数(增加、删除、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小,反之,两个字符串的相似程度越大。对于两个完全相同的字符串,编辑距离为0。
根据所包含编辑操作种类的不同,编辑距离有多种不同的计算方式,莱文斯坦距离 和 最长公共子串长度。莱文斯坦距离允许增加、删除、替换字符三个编辑操作,最长公共子串长度允许增加、删除字符两个操作。它们是从两个截然相反的角度,分析字符串的相似程度。莱文斯坦距离的大小,表示两个字符串的差异的大小;而最长公共子串长度的大小,表示两个字符串的相似度的大小。从下图可以看出,这两个字符串的莱文斯坦距离和最长公共子串长度分别是 3 和 4。
2. 编程计算莱文斯坦距离
这个问题是求吧一个字符串变成另一个字符串,需要的最少编辑次数。整个求解过程,涉及多个决策阶段,需要依次考察一个字符串中的每个字符,跟另一个字符串的字符是否匹配,匹配的话如何处理,不匹配的话又如何处理。所以这个这个问题符合“多阶段决策最优解”模型。
一般的,贪心、回溯、动态规划可以解决的问题,都能抽象成这样的模型。先来看看回溯算法是如何解决的。
首先,回溯是一个递归处理的过程。如果 a[i] 和 b[j] 匹配,我们递归考察 a[i+1] 和 b[j+1] 。如果不匹配,那我们有多种处理方式可选:
a) 可以删除a[i],然后递归考察 a[i+1] 和 b[j];
b) 可以删除b[j],然后递归考察 a[i] 和 b[j+1];
c) 在a[i] 前面添加跟 b[j] 相同的字符, 然后递归考察a[i] 和 b[j+1];
d) 在b[j] 前面添加跟 a[i] 相同的字符, 然后递归考察a[i+1] 和 b[j];
e) 将 a[i] 替换成 b[j] 或者将 b[j] 替换成 a[i],然后递归考察a[i+1] 和 b[j+1]。
看代码:
char a[] = "mitcmu";
char b[] = "mtacnu";
int m, n, minDist = 0; //minDist 存储结果
m = n = 6;
void lwstBT(int i, int j, inr edist) //调用方式lwstBT(0, 0, 0)
{
if(i==n || j == m)
{
if(i<n)
edist += n-i;
if(j<m)
edist += m-j;
if(edist < minDist)
minDist = edist;
return;
}
if(a[i] == b[j])
lwstBT(i+1, j+1, edist);
else //当不匹配的时候,以下三种分别对应上面叙述的情况
{
lwstBT(i+1, j, edist+1);
lwstBT(i, j+1, edist+1);
lwstBT(i+1, j+1, edist+1);
}
}
根据回溯算法代码的实现,可以画出递归树,看是否存在重复子问题。若存在,就可以考虑用动态规划来解决;若不存在,那么回溯算法就是最好的解决方法。
,
在递归树中,每个节点代表一种状态,状态包含三个变量(i, j, edist),其中edist表示处理 a[i] 和 b[j] 时,已经执行的编辑操作次数。对于(i, j)相同的节点,只需保留edist最小的,继续递归处理就可以了,剩下的节点可以舍弃。所以,(i, j, edist) 就变成了(i, j, min_edist),其中min_edist表示处理到a[i] 和 b[j] 时,已经执行的最少编辑次数。
我们可以写出状态转移方程:
如果:a[i] != b[j],那么:
min_edist(i, j) = min( min_edist(i-1, j)+1, min_edist(i, j-1)+1, min_edist(i-1, j-1)+1 )
如果:a[i] == b[j],那么:
min_edist(i, j) = min( min_edist(i-1, j)+1, min_edist(i, j-1)+1, min_edist(i-1, j-1) )
了解了状态间的递推关系,我们画出一个二维的状态表,按行依次填充表中的值。
代码如下:
int lwstDP()
{
int minDist[6][6] = {0};
for(int j=0; j<m; j++)
{
if(a[0] == b[j])
minDist[0][j] = j;
else if(j != 0)
minDist[0][j] = minDist[0][j-1] + 1; //a[0] != b[0] 这个时候j=0,但是minDist=1,所以这种情况要除外,用下面的else 来表达
else
minDist[0][j] = 1;
}
for(int i=0; i<n; i++)
{
if(a[i] == b[0])
minDist[i][0] = i;
else if(i != 0)
minDist[i][0] = minDist[i-1][0] + 1;
else
minDist[i][0] = 1;
}
for(int i=1; i<n; i++)
for(itn j=1; j<m; j++)
{
if( a[i] == b[j] )
minDIst = min( minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1] );
else
minDIst = min( minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1 );
}
return minDist[n-1][m-1];
}
3. 编程计算最长公共子串长度
最长公共子串长度作为编辑距离的一种,只允许增加、删除字符两种编辑操作。从本质上,表征的也是两个字符串之间的相似程度。这个问题的解决思路,跟莱文斯坦距离类似,也可以用动态规划解决。解决思路前面已经讲得非常详细,这里就直接来定义状态,,在写状态转移方程。
每个状态还是包括三个变量(i, j, max_lcs),max_lcs表示 a[0...i] 和 b[0...j] 的最长公共子串长度,那 (i, j) 这个状态是从那么哪些状态转移过来的呢?
先来卡回溯的回溯的处理思路。从 a[0] 和 b[0]开始,依次考察两个字符串的字符是否匹配:
a) 如果a[i] 和 b[j] 互相匹配,我们将最长公共子串长度+1,继续考察 a[i+1] 和 b[j+1]。
b) 如果a[i] 和 b[j] 不匹配,最长公共子串长度不变,有两种不同的决策路线:
删除a[i],或者在 b[j] 前面加上a[i],继续考察a[i+1] 和 b[j];
删除b[j],或者在 a[i] 前面加上b[j],继续考察a[i] 和 b[j+1]。
所以,状态(i, j)是从下面三个状态转移过来:
(i-1, j-1, max_lcs),其中max_lcs 表示a[0...i-1] 和 b[0...j-1] 的最长公共子串长度;
(i-1, j, max_lcs), 其中max_lcs 表示a[0...i-1] 和 b[0...j] 的最长公共子串长度;
(i, j-1, max_lcs),其中max_lcs 表示a[0...i] 和 b[0...j-1] 的最长公共子串长度。
状态转移方程就可以写出来:
如果a[i] == b[j],那么:
max_lcs(i, j) = max( max_lcs(i-1, j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1) );
如果a[i] != b[j], 那么:
max_lcs(i, j) = max( max_lcs(i-1, j-1), max_lcs(i-1, j), max_lcs(i, j-1) );
看代码:
int lcsDP(void)
{
char a[] = "mitcmu";
char b[] = "mtacnu";
int maxlcs[6][6] = {0};
int n, m;
n = m = 6;
for(int j=0; j<m; j++)
{
if(a[0] == b[j])
maxlcs[0][j] = 1;
else if(j != 0)
maxlcs[0][j] = maxlcs[0][j-1];
else
maxlcs[0][j] = 0;
}
for(int i=0; i<n; i++)
{
if(a[i] == b[0])
maxlcs[i][0] = 1;
else if(i != 0)
maxlcs[i][0] = maxlcs[i-1][0];
else
maxlcs[i][0] = 0;
}
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
{
if(a[i] == b[j])
maxlcs[i][j] = max( max[i-1][j-1]+1, max[i-1][j], max[i][j-1] );
else
maxlcs[i][j] = max( max[i-1][j-1], max[i-1][j], max[i][j-1] );
}
return maxlcs[n-1][m-1];
}
所以,当用户在搜索框内,输入一个拼写错误的单词时,我们就拿这个单词跟词库中的单词一一进行比较,计算编辑距离,将编辑距离最小的单词,作为纠正之后的单词,提示给用户。
上述是拼写纠错的最基本原理。不过,真正用于商用的搜索引擎,拼写纠错功能显然不会就这么简单。一方面,单纯利用编辑距离来纠错,效果并不一定好;另一方面,词库 中的数据量可能很大,手欧引擎每天支持海量的搜索,所以对纠错的性能要求很高。
针对纠错效果不好的问题,我们有很多的优化思路,介绍几种。
(1) 我们不仅仅取出编辑距离最小的那个单词,二是取出编辑距离最小的TOP 10,然后根据其他参数,决策选择哪个单词作为拼写纠错单词。比如使用搜索热门程度来决策。
(2) 我们还可以使用多种编辑距离计算方法,比如这里介绍的两种,然后分别求编辑距离最小的TOP 10,然后求交集,再对交集的结果,继续优化处理。
(3) 还可以通过统计用户的搜索日志,得到最常被拼错的单词列表,以及对应的拼写正确的单词,搜索引擎在拼写纠错的时候,首先在这个最常被拼错单词列表中查找。如果一旦找到,直接返回正确的单词。
(4)还有一种更高级的做法,引入个性因素。针对每个用户,维护这个用户特有的搜索喜好,也就是常用的关键词。当用户输入错误的单词时,首先在这个用户常用的搜索关键词中,计算编辑距离,查找编辑距离最小的单词。
针对纠错性能方面,也有相应的优化方式,将两种分治的优化思路。
(1) 如果纠错的TPS不高,我们可以部署多台机器,每台机器运行一个独立的纠错功能。当有一个纠错请求的时候,通过负载均衡,分配到其中一台机器,来计算编辑距离,得到纠错单词。
(2)如果纠错系统的响应时间太长,也就是,每个纠错请求处理时间时间过长,可以将就错的词库,分割到多台机器。当有一个纠错请求的时候,就将这个拼错单词,同时发给多台机器,并行处理,分别得到编辑距离最小的单词,然后再比对合并,最终决策出最优的拼错单词。
真正的搜索引擎拼写纠错优化,肯定不止这里介绍的这么简单,但是万变不离其宗。掌握了核心原理,就是掌握了解决问题的方法剩下就只灵活应用和实战操练了。
通过这篇文章的学习,相信一定能在动态规划方面达到入门级了。
以上所述,是本人最近在极客时间上学习数据结构和算法相关课程做的笔记吧。