文本相似度计算和最小修改匹配问题

文本相似度计算和最小修改匹配问题

序言

由于项目需要,需要写一个文本比较的算法,实现比对两个文本差异,计算两段文本的相似度,并给出最小的修改途径使得原文本修改后得到目标文本;走度娘处找到这么一个算法,作者没有道出算法名称,只知道是图论相关的。原文参考:https://blog.csdn.net/sunskyor/article/details/4491656

最大匹配度

最大匹配度说的是原文本和目标文本的最大匹配字符数。
这里直接上demo讲解:假设两个文本原文本(left):BCXCADFESBABCACA,目标文本(right):ABCACADF。
首先建立一张图来逐个比较两个文本,
在这里插入图片描述
上图中1就表示原文本与目标文本匹配的字符,0表示不匹配的字符。那么原问题就转换成从表格坐上角找到一条路径满足下面几个条件:

  1. 途经1的单元格最多;
  2. 每次只能向下,或者向右,或者右下移动一格。
  3. 如果本次格子上的值为1,那么只能向右下移动。
  4. 如果移动到右边界或下边界则终止。

这其实就是一个有条件搜索最大权重路径的问题。但是,这里讲的是文本匹配问题,和图论相比,要简单很多。因为文本是流式的,两个文本之间的所有匹配关系一定是一个很规则的矩阵,这比图论中研究的情况要简单多了。
最先想到的是什么呢?迭代和递归,是不是?别着急,没有那么复杂的,我们来分析一下,再做打算。
我们来分析一下,基本的思路是数学归纳法,呵呵,其实是递归算法的数学原型。边界上的单元,不用说了,一定是最多只能找到一个匹配点。而对于表格中的任意一个单元, 我们用 N(l,r) 来表示,对于它,按照上面的规则,它有3个邻接区域 A, B, C.

在这里插入图片描述
我们用N(l,r)来表示“将left的第L个元素和right的第R个元素匹 配后,能够获取的最大匹配点数”。这个表述有点难以理解,从前面的“找到一个路径…”的观点出发,我们还可以这么说明N(l,r)的含义:“从第L行 R列的单元格出发,满足所有4个条件的路径上能够经过的值为"1"的单元个数”。
因为N(l,r)的下一步一定是区域A,B,C中的一个,而且,如果(l,r)是一个匹配点,只能选择进入A区域;如果进入B,C,则(l,r)一定不是一个匹配点。因此,我们可以得到:
N(l,r) = Max( V(l,r)+N(区域A), N(区域B), N(区域C) ) 。"V(l,r)表示单元(l.r)的值,=0表示单元(l,r)不是一个匹配点,=1表示单元(l,r)是一个匹配点"

而一个区域的最大匹配点数,就是从该区域的入口点出发,所能得到的最大匹配点数,即:N(区域[(a,b),(c,d)]) = N(a,b). "区域[(a,b),(c,d)]的意思是:由点(a,b) 和点(c,d)所构成的矩形区域)"
那么,前式就变成了:
N(l,r) = Max( V(l.r)+N(区域A), N(区域B), N(区域C) )
= Max( N(l+1,r+1)+V(l,r) , N(l,r+1), N(l+1,r))
在Excel上实现一把,看到最大匹配数是6,可以看到最大匹配串有下面那四条:

在这里插入图片描述
至于代码如何实现后面找到最小修改路径再一并贴。

最短匹配路径

一个文本到另一个文本的修改途径是多种多样的,我们现在需要找到一个修改最小的途径,从图上可以看出最短途径是绿色那条线,单纯从原文本中截取就可以得到目标文本。
下面分析如何得到最短路径,这个地方我就原文引用原作者的文字了:https://blog.csdn.net/sunskyor/article/details/4491654
确定最优匹配路径的问题,通常在做文件比较时要用到,它的意思是:在所有能够得到最大匹配点数的路径中,找出一条最短的路径。
首先,我们仍然是手工标出能够得到最大匹配点数的所有路径。在手工绘制可用路径的时候,需要说明一下:

首先,这里我们以计算 left 的最优路径为例。计算 right 的最优路径和 left 是完全对称的;

在图中,向右移动一格,表示 left 的当前元素放弃和 right 的当前元素进行匹配,而和 right 的下一个元素进行匹配。如果进行文件比较的话,就是 left 文件的当前行前,插入 right 文件的当前行;

在图中,向下移动一格,表示 left 的当前元素放弃和 right 的当前元素进行匹配,而使用 left 的下一个元素和 right 的当前元素进行匹配。如果进行文件比较的话,就是 left 文件的当前行被删除;

在途中,向右下方移动一格,表示确定 left 和 right 的当前元素的匹配,使用 left 的下一个元素和 right 的下一个元素进行匹配。如果进行文件比较的话,就是 left 和 right 文件的当前行已经匹配,分别比较下一行。

由于 left 和 right 是流式的,所以每个元素只能匹配一次而且不能颠倒顺序,所以,在 x 和 y 方向上,只能保留一个匹配点,而且只能向下,向右,或者向右下方移动。

有了这些规则,我们就可以得到如下图中间红色粗线所示的 4 条可能的路径。
在这里插入图片描述
从上向下,依次编号为 1,2,3,4 号路径,分析如下:
1 号路径表示(只说明含义,算法推导在后面):
在这里插入图片描述
依次类推,可以得到其他路径的实际含义。需要说明的是, 3 号路径在选择第二个匹配点时,没有采用 left[9]:right[1] 的匹配关系,而是跳过 left[9] ,采用了 left[11]:right[1] 。这样,点 (left[9], right[1]) 在 3 号路径上是“删除 left 第 9 行”的意思。

如果我将整个过程中在 left 上移动的距离纪录下来,就得到:
在这里插入图片描述
从图上就很容易看出,在 left 上移动最短的,也就是最优的路径,是 4 号路径。
是不是看起来毫无头绪?
别急,象上次一样,我们进行一个分析。
依然是归纳法,依然是找出元素 D(l,r) 的 3 个相邻区域 A,B,C:
在这里插入图片描述
分析:

如果点 D(l,r) 表示从 left 的第 L 个元素, right 的第 R 个元素出发,匹配到矩阵边界后,在 left 方向上的最短路径长度 (我更喜欢称之为 depth ) ,那么:
如果点 (l,r) 是一个被确定的匹配点,那么下一步,只能选择进入 A 区域,到点 (l+1,r+1) ,所以得到:
D1 If (V(l,r)>0) then D(l,r) = 1 + D(l+1,r+1)
还记得 V(l,r) 和 N(l,r) 的定义么?可以看看文档《文本比较算法剖析( 1 ) - 如何确定最大匹配率 》
如果点 (l,r) 不是一个被确定的匹配点,那么下一步可以进入 B 或者 C.
D2 如果进入 B 区域,那么意味着 ” 插入 right 的第 r 行 ” ,但是在 left 的位置保持不变,所以得到:
If (V(l,r) == 0) then D(l,r) = D(l,r+1)
D3 如果进入 C 区域,那么意味着 ” 删除 left 的第 l 行 ” ,在 right 的位置保持不变,但是在 left 的位置要加 1 。所以得到 :
If (V(l,r) == 0) then D(l,r) = 1 + D(l+1,r)
那么现在有 3 个值 D1 , D2 , D3 ,该如何取舍呢?直接取 Min(D1,D2,D3) 可以么?

呵呵,其实,我也是尝试了很多次以后才搞清楚的。大家可以用 excel 验证一下自己的想法。很简单的,大概验证整个算法,用 10 行左右的代码再加上 excel 本身提供的公式就足够了。

不要忘记了前面说过的, ” 在所有能够得到最大匹配点数的路径中,找出一条最短的路径 ”, 首先我们要保证得到最大匹配匹配点数,所以我们又有:

If (N(l,r+1) >= N (l+1,r) then
D(l,r) = D(l,r+1);
Else
   D(l+r) = 1 + D(l+1,r)

综合上面所有的分析,路径长度 D(l,r) 的计算公式就是:

If (V(l,r) = 1) Then
    D(l,r) = D(l+1,r+1) + 1
Else
    If (N(l,r+1) >= N(l+1,r)) Then
        D(l,r) = D(l,r+1)
    Else
        D(l,r) = D(l+1,r) + 1
    End If

将公式写入 excel 自动计算,得到结果如图:
在这里插入图片描述
可以看到,结果正确。
OK 。 结束了,文本比较的算法核心就介绍完了。
事实上,上面第四步给出的是优化后的结果,有兴趣的可以自己试着分析一下第 4 步是否应该这样计算,有什么情况我没有在这里讲的,但是结果为什么又是正确的。
有了这个算法,大家可以很快的编写自己的文本比较功能了。
计算最优路径的附加时间复杂度为 0 ,因为它完全可以和计算最大匹配点数一起进行;附加的空间复杂度为 Max(m,n), 因为它需要一个额外的数组纪录 N(l+1) 。
如果不计算最优路径的话,计算最大匹配点数的算法还可以再优化。

实现代码

下面是我针对我的场景写的 实现代码,仅供参考,若有错误,请指出。基本上看注释都能懂,若有疑问请留言。

原文本:民事活动应当遵循自愿、公平、等价有偿、诚实信用的原则
目标文本:民事主体从事民事活动,应当遵循自愿原则,按照自己的意思设立、变更、终止民事法律关系
输出结果:

{"compStr":"民事<add>主体从事民事</add>活动<add>,</add>应当遵循自愿<delete>、公平、等价有偿、诚实信用的</delete>原则<add>,按照自己的意思设立、变更、终止民事法律关系</add>","similarity":0.2926829268292683}

/**
 * <br>类描述:字符串比较,输入待比较的字符串,输出字符串的相似度,和最小修改途径
 * <br>author: lwl liuwanli_eamil@163.com	2021/7/30 13:41
 *
 * @ClassName StringComparison 
 * @see #
 * @since {修改人、修改时间、修改事由}
 */
public class StringComparison {

    private static String ADD_START = "<add>";
    private static String ADD_END = "</add>";
    private static String DELETE_START ="<delete>";
    private static String DELETE_END ="</delete>";

    public static void main(String[] args) {
        System.out.println(comparsion("民事活动应当遵循自愿、公平、等价有偿、诚实信用的原则","民事主体从事民事活动,应当遵循自愿原则,按照自己的意思设立、变更、终止民事法律关系"));
    }

    /**
     * 比较源字符串和目标字符串,得到变化结果
     * @param source
     * @param desc
     * @return {“compStr”:“比较后的结果”,“similarity”:“两个字符串的相似度”}
     */
    public static JSONObject comparsion(String source, String desc){
        JSONObject res = new JSONObject();
        if(source==null || source.length()<1){
            res.set("compStr", ADD_START + desc + ADD_END);
            //相似度-1,目标完全是新增的。
            res.set("similarity", -1);
            return res;
        }
        if(desc == null || desc.length()<1){
            res.set("compStr", DELETE_START + source + DELETE_END);
            //相似度-2,目标为空,删除源文本
            res.set("similarity", 2);
            return res;
        }
        if(source.equals(desc)){
            res.set("compStr",source);
            res.set("similarity", 1);
            return res;
        }
        int socurceLen = source.length();
        int descLen = desc.length();

        //初始化一个数组,目标为行,源为列
        int[][] compMap = new int[socurceLen+1][descLen+1];
        //记录匹配度
        int[][] maxMatchMap = new int[socurceLen+1][descLen+1];
        //记录差异步数
        int[][] minPathMap = new int[socurceLen+1][descLen+1];
        //比对两个字符串,构建图数据
        for(int i=0;i<socurceLen;i++){
            for(int j=0;j<descLen;j++){
                compMap[i][j] = source.charAt(i) == desc.charAt(j)?1:0;
                maxMatchMap[i][j] = compMap[i][j];
                minPathMap[i][j] = compMap[i][j];
            }
        }

        //自下往上,自左往右开始计算每个字符开始比较的最大匹配字符数,将其填充到图中
        for(int i= socurceLen-1;i>=0;i--){
            for(int j= descLen-1;j>=0;j--){
                maxMatchMap[i][j] = Math.max(maxMatchMap[i][j]+maxMatchMap[i+1][j+1],Math.max(maxMatchMap[i][j+1],maxMatchMap[i+1][j]));
            }
        }
        //自下往上,自左往右计算差异同化所需步数
        for(int i=socurceLen-1;i>=0;i--){
            for(int j=descLen-1;j>=0;j--){
                if(minPathMap[i][j] == 1){
                    minPathMap[i][j] = minPathMap[i+1][j+1]+1;
                }else if(minPathMap[i][j+1] >= minPathMap[i+1][j]){
                    minPathMap[i][j] = minPathMap[i][j+1];
                }else{
                    minPathMap[i][j] = minPathMap[i+1][j]+1;
                }
            }
        }
        int maxMatchSize = maxMatchMap[0][0];

        //寻找满足最短路径最大匹配的点(即原句子删除最少能匹配上目标句子的路径)
        int minPath = socurceLen+descLen;
        for(int i=0;i<socurceLen-1;i++){
            for(int j=0;j<descLen-1;j++){
                //该点是能最大匹配点
                if(compMap[i][j] == 1 && maxMatchMap[i][j] == maxMatchSize){
                    //满足路径最短
                    if(minPath > minPathMap[i][j]){
                        minPath = minPathMap[i][j];
                    }
                }
            }
        }
        int flag = 0;
        StringBuilder resBuilder = new StringBuilder();
        int i=0;
        int j=0;

        while(!(i>socurceLen-1 && j>descLen-1)){
            if(compMap[i][j] == 1){
                flag = matchedChar(resBuilder,source.charAt(i),flag);
                //匹配上一个了,剩余匹配度减一,同样的,最小路径长度也需要减一
                i++;j++;
                //对下一个比对点进行比对
                continue;
            }
            //没匹配上,看下一步往下还是往右走,在保证匹配度一致的情况下选择最短路径
            //如果当前无法下行了,那么只能往右
            if(i>socurceLen-1){
                //右行,新增字符
                flag = addChar(resBuilder,desc.charAt(j),flag);
                j++;
                continue;
            }
            //如果当前无法右行了,那么只能往下
            if(j>descLen-1){
                //下行,删字符
                flag = deleteChar(resBuilder,source.charAt(i),flag);
                i++;
                continue;
            }

            //如果选择下行会降低匹配度,说明应该往右(如果下行不降低匹配度,那指定往下行,应为下行可以缩短路径)
            if(maxMatchMap[i][j] > maxMatchMap[i+1][j] ){
                //右行,新增字符
                flag = addChar(resBuilder,desc.charAt(j),flag);
                j++;
            }else{
                //下行,删字符
                flag = deleteChar(resBuilder,source.charAt(i),flag);
                i++;
            }
        }
        resBuilder.append(flag==-1?DELETE_END:(flag==1?ADD_END:""));
        res.set("compStr", resBuilder.toString());
        //相似度-1,目标完全是新增的。
        res.set("similarity", maxMatchSize * 1.0 / descLen);
        return res;
    }

    private static int deleteChar(StringBuilder resBuilder,char c,int flag){
        //删除字符
        if(flag == 1){
            resBuilder.append(ADD_END);
            flag = 0;
        }
        if(flag == 0){
            resBuilder.append(DELETE_START);
            flag = -1;
        }
        resBuilder.append(c);
        return flag;
    }

    private static int addChar(StringBuilder resBuilder,char c,int flag){
        if(flag == -1){
            resBuilder.append(DELETE_END);
            flag = 0;
        }
        if(flag == 0){
            resBuilder.append(ADD_START);
        }
        flag = 1;
        resBuilder.append(c);
        return flag;
    }

    private static int matchedChar(StringBuilder resBuilder,char c,int flag){
        if(flag == 1){
            resBuilder.append(ADD_END);
        }
        if(flag == -1){
            resBuilder.append(DELETE_END);
        }
        resBuilder.append(c);
        return 0;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值