📅  最后修改于: 2023-12-03 15:36:56.978000             🧑  作者: Mango
K 数组,全称KMP匹配算法(Knuth-Morris-Pratt Algorithm),是一种字符串匹配算法。它的核心思想是,当出现字符不匹配时,让模式串尽可能的移动少的位数,以达到快速匹配的目的。
给定两个字符串 A 和 B,设A的K数组为a,B的K数组为b,现在要从A,b中选择任意位置删去后缀(可以不删),使得删除后的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_sa
和 cal_height
分别对字符串 s
计算后缀数组和 height 数组,RMQ
是RMQ算法的简单实现。min_common_sum
是求解问题的主函数。