zoukankan      html  css  js  c++  java
  • 动态规划(四)——如何实现搜索引擎中的拼写纠错功能?

      如何量化两个字符串之间的相似程度呢?有一个非常著名的量化方法,那就是编辑距离(Edit Distance)。

      编辑距离指的就是,将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。对于两个完全相同的字符串来说,编辑距离就是 0。

      编辑距离有多种不同的计算方式,比较著名的有莱文斯坦距离(Levenshtein distance)和最长公共子串长度(Longest common substring length)。其中,莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,最长公共子串长度只允许增加、删除字符这两个编辑操作。

      莱文斯坦距离和最长公共子串长度,从两个截然相反的角度,分析字符串的相似程度:

    • 莱文斯坦距离的大小,表示两个字符串差异的大小

    • 最长公共子串的大小,表示两个字符串相似程度的大小

      根据回溯算法的代码实现,我们可以画出递归树,看是否存在重复子问题。如果存在重复子问题,那我们就可以考虑能否用动态规划来解决;如果不存在重复子问题,那回溯就是最好的解决方法。

    C++版代码如下

    #include <iostream>
    #include <math.h>
    #include <string.h>
    using namespace std;
    
    #define MAXNUM 100010
    #define DRIFT 1001
    
    int minWeight = 9999;
    int minDist = 0xFFFFFF;
    
    // 回溯法求最短路径
    void reCall(int cost[][4], int rows, int cols, int row, int col, int curS){
        if((row == (rows - 1)) && (col == (cols - 1))){
            //cout<<curS<<endl;
            if(curS < minWeight)
                minWeight = curS;
            return ;
        }
        // 向右走
        if(col < cols - 1)
            reCall(cost, rows, cols, row, col + 1,curS + cost[row][col + 1]);
        // 向下走
        if(row < rows - 1)
            reCall(cost, rows, cols, row + 1, col, curS + cost[row + 1][col]);
    }
    
    // 动态规划版
    int dpRoad(int cost[][4], int n, int row, int col){
        int dp[n][n];
        memset(dp, -1, sizeof(dp));
        // 初始化第一行、第一列
        dp[0][0] = cost[0][0];
        for(int i = 1; i < n; i++){
            dp[0][i] = dp[0][i - 1] + cost[0][i];
            dp[i][0] = dp[i - 1][0] + cost[i][0];
        }
    
        for(int i = 1; i < n; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = cost[i][j] + min(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[row - 1][col - 1];
    }
    
    // 回溯法求莱文斯坦编辑距离
    void lwstBT(char str_1[], int n, char str_2[], int m, 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 ;
        }
        // 1、两字符相匹配
        if(str_1[i] == str_2[j])
            lwstBT(str_1, n, str_2, m, i + 1, j + 1, edist);
        else{
            // 2、删除a[i]或者b[j]前添加一个字符
            lwstBT(str_1, n, str_2, m, i + 1, j, edist + 1);
            // 3、删除b[j]或者a[i]前添加一个字符
            lwstBT(str_1, n, str_2, m, i, j + 1, edist + 1);
            // 4、替换
            lwstBT(str_1, n, str_2, m, i + 1, j + 1, edist + 1);
        }
    }
    
    int myMin(int x, int y, int z) {
        int minv = 0x7FFFFFFF;
        if (x < minv)
            minv = x;
        if (y < minv)
            minv = y;
        if (z < minv)
            minv = z;
        return minv;
    }
    
    int myMax(int x, int y, int z) {
        int maxv = -1;
        if (x > maxv) maxv = x;
        if (y > maxv) maxv = y;
        if (z > maxv) maxv = z;
        return maxv;
    }
    
    // 动态规划法求莱文斯坦编辑距离
    int lwstDP(char a[], int n, char b[], int m) {
        int minDist[n][m];
        memset(minDist, -1, sizeof(minDist));
        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] = myMin(minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]);
                else
                    minDist[i][j] = myMin(minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1);
            }
        }
    
        return minDist[n-1][m-1];
    }
    
    
    int lcsDP(char a[], int n, char b[], int m) {
        int maxlcs[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] = myMax(maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]+1);
                else
                    maxlcs[i][j] = myMax(maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]);
            }
        }
    
        return maxlcs[n-1][m-1];
    }
    
    // 动态规划求 a 的最上升长子序列长度
    int lengthOfLIS(int nums[], int n){
    
        vector<int> dp(n, 1);
    
        for(int i = 0; i < n; i++){
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i])
                    dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    
        int ans = INT_MIN;
        for(int i = 0; i < n; i++)
            ans = max(ans, dp[i]);
    
        return ans;
    }
    
    int main()
    {
    
        int cost[4][4]={{1, 3, 5, 9},{2, 1, 3, 4},{5, 2, 6, 7},{6, 8, 4, 3}};
    
        // 从(0, 0)开始走到(n-1, n-1),初始值路径值为1
        reCall(cost, 4, 4, 0, 0, 1);
        cout<<minWeight<<endl;
        cout<<dpRoad(cost, 4, 4, 4)<<endl;
    
        // 最长公共子串(从前到后)"mtcu"4个
        char str_1[7] = "mitcmu";
        char str_2[7] = "mtacnu";
        lwstBT(str_1, 6, str_2, 6, 0, 0, 0);
        cout<<minDist<<endl;
        cout<<lwstDP(str_1, 6, str_2, 6)<<endl;
        cout<<lcsDP(str_1, 6, str_2, 6);
        return 0;
    }
    

    解答开篇

      当用户在搜索框内,输入一个拼写错误的单词时,我们就拿这个单词跟词库中的单词一一进行比较,计算编辑距离,将编辑距离最小的单词,作为纠正之后的单词,提示给用户。

      这就是拼写纠错最基本的原理。不过,真正用于商用的搜索引擎,拼写纠错功能显然不会就这么简单。一方面,单纯利用编辑距离来纠错,效果并不一定好;另一方面,词库中的数据量可能很大,搜索引擎每天要支持海量的搜索,所以对纠错的性能要求很高。

      针对纠错效果不好的问题,我们有很多种优化思路:

    • 我们并不仅仅取出编辑距离最小的那个单词,而是取出编辑距离最小的 TOP 10,然后根据其他参数,决策选择哪个单词作为拼写纠错单词。比如使用搜索热门程度来决定哪个单词作为拼写纠错单词。

    • 我们还可以用多种编辑距离计算方法,比如今天讲到的两种,然后分别编辑距离最小的 TOP 10,然后求交集,用交集的结果,再继续优化处理。

    • 我们还可以通过统计用户的搜索日志,得到最常被拼错的单词列表,以及对应的拼写正确的单词。搜索引擎在拼写纠错的时候,首先在这个最常被拼错单词列表中查找。如果一旦找到,直接返回对应的正确的单词。这样纠错的效果非常好。

    • 我们还有更加高级一点的做法,引入个性化因素。针对每个用户,维护这个用户特有的搜索喜好,也就是常用的搜索关键词。当用户输入错误的单词的时候,我们首先在这个用户常用的搜索关键词中,计算编辑距离,查找编辑距离最小的单词。

      针对纠错性能方面,我们也有相应的优化方式。我讲两种分治的优化思路。

    • 如果纠错功能的 TPS 不高,我们可以部署多台机器,每台机器运行一个独立的纠错功能。当有一个纠错请求的时候,我们通过负载均衡,分配到其中一台机器,来计算编辑距离,得到纠错单词。

    • 如果纠错系统的响应时间太长,也就是,每个纠错请求处理时间过长,我们可以将纠错的词库,分割到很多台机器。当有一个纠错请求的时候,我们就将这个拼写错误的单词,同时发送到这多台机器,让多台机器并行处理,分别得到编辑距离最小的单词,然后再比对合并,最终决定出一个最优的纠错单词。

      真正的搜索引擎的拼写纠错优化,肯定不止这么简单,但是万变不离其宗。掌握了核心原理,就是掌握了解决问题的方法。

  • 相关阅读:
    LeetCode分类专题(五)——动态规划1-子序列
    LeetCode分类专题(四)——双指针和滑动窗口1
    LeetCode分类专题(三)——二分查找1
    消息队列(一)——Kafka概述
    Java多线程(五)——synchronized关键字原理
    Java多线程(四)——volatile关键字原理
    Redis(五)——主从复制、哨兵
    Redis(四)——过期、持久化、事件
    Redis(三)——底层数据结构
    MySQL知识点
  • 原文地址:https://www.cnblogs.com/flyingrun/p/13511965.html
Copyright © 2011-2022 走看看