📌  相关文章
📜  删除部分后缀后 K 数组的最小公共总和(1)

📅  最后修改于: 2023-12-03 15:36:56.978000             🧑  作者: Mango

删除部分后缀后 K 数组的最小公共总和

什么是 K 数组?

K 数组,全称KMP匹配算法(Knuth-Morris-Pratt Algorithm),是一种字符串匹配算法。它的核心思想是,当出现字符不匹配时,让模式串尽可能的移动少的位数,以达到快速匹配的目的。

删除部分后缀后 K 数组的最小公共总和是什么?

给定两个字符串 A 和 B,设A的K数组为a,B的K数组为b,现在要从A,b中选择任意位置删去后缀(可以不删),使得删除后的K数组相同,求使得被删的后缀的长度和最小的方案,即是所谓的“删除部分后缀后K数组的最小公共总和”。

如何求解删除部分后缀后 K 数组的最小公共总和?

我们可以从一个简单的例子开始考虑。

假设字符串 A 为 abcab,它的K数组为[0, 0, 0, 1, 2]。字符串 B 为 aabca,它的K数组为[0, 1, 0, 0, 1]。容易发现,如果我们删去字符串 A 的最后两个字母,它的K数组变为[0, 0, 0, 1],与字符串 B 的K数组相同。

我们可以把字符串 A 和字符串 B 连起来,中间加一个特殊字符,比如说 #,得到新字符串 C = abca#aabca。用双指针同时遍历字符串 C,对于C的每个位置i,记A的到i为止的K数组为a[0..i],B的到j为止的K数组为b[0..j]。我们可以枚举在 i 处删去A的后缀和在 j 处删去B的后缀,使得a[0..i]和b[0..j]相同。

具体来说,我们可以在两个指针 i 和 j 之间插入一个断点,将字符串 C 切成两个部分。左侧部分的长度为i+k,其中k为被删掉的 A 的后缀长度;右侧部分的长度为j+l,其中l为被删掉的 B 的后缀长度。

那么现在的问题转化成了,如何在两个字符串的K数组之间找到一个最长的公共前缀。这个问题可以用后缀数组和RMQ算法来解决。

具体地,我们对于 C求它的后缀数组和height数组。height数组是相邻两个后缀的最长公共前缀的长度。我们可以将 height 数组中所有大于0的值分成若干段,每一段中出现的位置构成一个区间。为了便于查询区间最小值,我们可以使用 ST表 或 线段树 进行处理。

在求出区间最小值之后,我们就可以知道最长的公共前缀的长度是多少。然后我们再回到原来的问题,根据 C 中的断点位置,计算出A、B 被删去的后缀长度。对于每个位置 i,我们都这样计算一遍,然后取最小值即可得到答案。

代码实现
def min_common_sum(a:str, b:str) -> int:
    C = a + '#' + b
    n = len(C)
    sa = cal_sa(C)
    height = cal_height(C, sa)
    rmq = RMQ(height)
    ans = float('inf')
    for i in range(len(a)+1):
        for j in range(len(b)+1):
            k = rmq.query(min(sa[i], sa[n-j-1])+1, max(sa[i], sa[n-j-1])+1)
            ans = min(ans, i+j-k)
    return ans

def cal_sa(s:str) -> List[int]:
    n = len(s)
    sa = [0] * n
    rk = [0] * n
    tmp = [0] * n
    for i in range(n):
        sa[i], rk[i] = i, ord(s[i])
    for k in range(1, n, k<<1):
        def cmp(x:int, y:int) -> bool:
            if rk[x] != rk[y]:
                return rk[x] < rk[y]
            if x+k<n and y+k<n:
                return rk[x+k] < rk[y+k]
            return x > y
        sa.sort(key=cmp_to_key(cmp))
        tmp[sa[0]] = 0
        for i in range(1, n):
            tmp[sa[i]] = tmp[sa[i-1]]+(cmp(sa[i-1], sa[i])!=0)
        tmp, rk = rk, tmp
        if rk[sa[n-1]] == n-1:
            break
    return sa

def cal_height(s:str, sa:List[int]) -> List[int]:
    n = len(s)
    height = [0] * n
    rk = [0] * n
    for i in range(n):
        rk[sa[i]] = i
    k = 0
    for i in range(n):
        if rk[i] == 0:
            continue
        if k > 0:
            k -= 1
        j = sa[rk[i]-1]
        while i+k < n and j+k < n and s[i+k] == s[j+k]:
            k += 1
        height[rk[i]] = k
    return height

class RMQ:
    def __init__(self, a:List[int]) -> None:
        n = len(a)
        logn = (n-1).bit_length()
        st = [[0] * n for _ in range(logn)]
        st[0] = a[:]
        for i in range(1, logn):
            for j in range(n-(1<<i)+1):
                st[i][j] = min(st[i-1][j], st[i-1][j+(1<<i-1)])
        self._st = st
        self._logn = logn

    def query(self, l:int, r:int) -> int:
        t = self._logn - 1
        while (1<<t) > r-l:
            t -= 1
        return min(self._st[t][l], self._st[t][r-(1<<t)])

其中 cal_sacal_height 分别对字符串 s 计算后缀数组和 height 数组,RMQ 是RMQ算法的简单实现。min_common_sum 是求解问题的主函数。