动态规划实战
新的搜索问题:搜索引擎纠错功能实现?
- 量化字符串相似度--》编辑距离:将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)
- 莱文斯坦距离(Levenshtein distance):允许增加、删除、替换字符这三个编辑操作
- 最长公共子串长度(Longest common substring length):允许增加、删除字符这两个编辑操作。
- 举例:两个字符串 mitcmu 和 mtacnu 的莱文斯坦距离是 3,最长公共子串长度是 4。
编程计算莱文斯坦距离
- 问题分析:这个问题是求把一个字符串变成另一个字符串,需要的最少编辑次数。整个求解过程,涉及多个决策阶段,我们需要依次考察一个字符串中的每个字符,跟另一个字符串中的字符是否匹配,匹配的话如何处理,不匹配的话又如何处理。所以,这个问题符合多阶段决策最优解模型。
- 多阶段求最优解,按前面的理论,先套用回溯算法,再画递归树,分析重复子问题,再看是否可列状态转换方程,再填二维状态表,最后翻译为代码实现。
- 回溯是一个递归处理的过程:
- 如果 a[i] 与 b[j] 匹配,我们递归考察 a[i+1] 和 b[j+1]。
- 如果 a[i] 与 b[j] 不匹配,那我们有多种处理方式可选:
- 可以删除 a[i],然后递归考察 a[i+1] 和 b[j];
- 可以删除 b[j],然后递归考察 a[i] 和 b[j+1];
- 可以在 a[i] 前面添加一个跟 b[j] 相同的字符,然后递归考察 a[i] 和 b[j+1];
- 可以在 b[j] 前面添加一个跟 a[i] 相同的字符,然后递归考察 a[i+1] 和 b[j];
- 可以将 a[i] 替换成 b[j],或者将 b[j] 替换成 a[i],然后递归考察 a[i+1] 和 b[j+1]。
- 翻译为代码:
-
private char[] a = "mitcmu".toCharArray(); private char[] b = "mtacnu".toCharArray(); private int n = 6; private int m = 6; private int minDist = Integer.MAX_VALUE; // 存储结果 // 调用方式 lwstBT(0, 0, 0); public lwstBT(int i, int j, int edist) { 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); // 删除a[i]或者b[j]前添加一个字符 lwstBT(i, j + 1, edist + 1); // 删除b[j]或者a[i]前添加一个字符 lwstBT(i + 1, j + 1, edist + 1); // 将a[i]和b[j]替换为相同字符 } }
- 画递归树判断重复子问题:
-
在递归树中,每个节点代表一个状态,状态包含三个变量 (i, j, edist),其中,edist 表示处理到 a[i] 和 b[j] 时,已经执行的编辑操作的次数。
- (i, j) 两个变量重复的节点很多,比如 (3, 2) 和 (2, 3)。对于 (i, j) 相同的节点,我们只需要保留 edist 最小的,继续递归处理就可以了,剩下的节点都可以舍弃。
- 所以,状态就从 (i, j, edist) 变成了 (i, j, min_edist),其中 min_edist 表示处理到 a[i] 和 b[j],已经执行的最少编辑次数。
-
- 状态转移方程
- 本例中,状态 (i, j) 可能从 (i-1, j),(i, j-1),(i-1, j-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)+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)) 其中,min表示求三数中的最小值。
- 状态表
- 翻译为代码实现
-
public int lwstDP(char[] a, int n, char[] b, int m) { int[][] minDist = new int[n][m]; for (int j = 0; j < m; ++j) { // 初始化第0行:a[0..0]与b[0..j]的编辑距离 if (a[0] == b[j]) minDist[0][j] = j; else if (j != 0) minDist[0][j] = minDist[0][j-1]+1; else minDist[0][j] = 1; } for (int i = 0; i < n; ++i) { // 初始化第0列:a[0..i]与b[0..0]的编辑距离 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 (int j = 1; j < m; ++j) { if (a[i] == b[j]) minDist[i][j] = min( minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]); else minDist[i][j] = min( minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1); } } return minDist[n-1][m-1]; } private int min(int x, int y, int z) { int minv = Integer.MAX_VALUE; if (x < minv) minv = x; if (y < minv) minv = y; if (z < minv) minv = z; return minv; }
-
铮哥技巧
- 拿到一个问题的时候,我们可以先不思考,计算机会如何实现这个问题,而是单纯考虑“人脑”会如何去解决这个问题。
- 人脑比较倾向于思考具象化的、摸得着看得见的东西,不适合思考过于抽象的问题。所以,我们需要把抽象问题具象化。
- 那如何具象化呢?我们可以实例化几个测试数据,通过人脑去分析具体实例的解,然后总结规律,再尝试套用学过的算法,看是否能够解决。
- 除此之外,我还有一个非常有效、但也算不上技巧的东西,我也反复强调过,那就是多练。实际上,等你做多了题目之后,自然就会有感觉,看到问题,立马就能想到能否用动态规划解决,然后直接就可以寻找最优子结构,写出动态规划方程,然后将状态转移方程翻译成代码。
编程计算最长公共子串长度
- 同理,先分析:
- 每个状态还是包括三个变量 (i, j, max_lcs),max_lcs 表示 a[0…i] 和 b[0…j] 的最长公共子串长度。
- 回溯的处理思路。
- 从 a[0] 和 b[0] 开始,依次考察两个字符串中的字符是否匹配。
- 如果 a[i] 与 b[j] 互相匹配,
- 将最大公共子串长度加一,并且继续考察 a[i+1] 和 b[j+1]。
- 如果 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]。
- 就是说求 a[0…i] 和 b[0…j] 的最长公共长度 max_lcs(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)); 其中max表示求三数中的最大值。
-
- 代码实现
-
public int lcs(char[] a, int n, char[] b, int m) { int[][] maxlcs = new int[n][m]; for (int j = 0; j < m; ++j) {//初始化第0行:a[0..0]与b[0..j]的maxlcs 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) {//初始化第0列:a[0..i]与b[0..0]的maxlcs 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 = 1; i < n; ++i) { // 填表 for (int j = 1; j < m; ++j) { if (a[i] == b[j]) maxlcs[i][j] = max( maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]+1); else maxlcs[i][j] = max( maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]); } } return maxlcs[n-1][m-1]; } private int max(int x, int y, int z) { int maxv = Integer.MIN_VALUE; if (x > maxv) maxv = x; if (y > maxv) maxv = y; if (z > maxv) maxv = z; return maxv; }
-
搜索单词纠错分析:
- 将搜索词分别与词库中的单词求编辑距离,将编辑距离最小的列出,提示给用户。
- 优化:
- 问题:单纯利用编辑距离来纠错,效果并不一定好;词库中的数据量可能很大,搜索引擎每天要支持海量的搜索,所以对纠错的性能要求很高
- 效果优化:
- 取出编辑距离最小的 TOP 10,再根据其他因素做决策哪个做为纠错单词。
- 使用多种编辑距离计算方法,取TOP 10,取交集
- 统计用户习惯信息:在个人常用词库中求编辑距离最小的单词。
- 性能优化:
- 多机部署并行计算,归并统计结果。
- 词库大:分库分表