最长重复子串
给你一个字符串
s
,考虑其所有 重复子串 ,即s
的(连续)子串,在s
中出现2次或更多次。这些出现之间可能存在重叠。
返回 任意一个 可能具有最长长度的重复子串。如果s
不含重复子串,那么答案为""
。
示例1:
输入:s = “banana”
输出:“ana”
示例2:
输入:s = “abcd”
输出:“”
提示:
- 2 < = s . l e n g t h < = 3 ∗ 1 0 4 2 <= s.length <= 3 * 10^4 2<=s.length<=3∗104
s
由小写英文字母组成
算法思想
该问题求解运用了二分、哈希、滑动窗口的算法思想
二分法
该问题需要求解的是重复子串的最大长度length,其范围在[0,len(s)-1]之间,由于该问题具有明显的分界,处于[0,length]之间均能找到重复子串,[length,len(s)-1]之间均无重复子串,可以考虑使用二分法进行处理(注意二分法的边界问题)。问题求解的基本框架为:
while left <= right:
mid = (left + right) // 2
if check(mid): # 此处check仅表示是否存在长度为mid重复子串,不同于正式实现时的check函数
left = mid + 1
记录重复子串的起始索引strat和长度length
else:
right = mid - 1
return s[start : start+length]
Rabin-Karp算法(字符串哈希)
Rabin-Karp算法是一种哈希,为一个字符串映射一个哈希值。
首先为每一个字符(小写字母)分配一个数字,将其视为26进制数,在将其转化为十进制表示,将该结果作为字符串的哈希值。判断是否为重复子串时,只需将两个字符串的哈希值进行比较即可(1.由于字符串长度较大,在计算时一般要进行取模操作;2.由于进行了取模操作,不同的字符串有可能在取模后回对应同一个哈希值,造成误判,可以选择较大的质数作为模数或选取多个模数)。
滑动窗口
在计算哈希值时,如果对每一个子串都按照进制转换方法计算,计算量会很大。
对于子串bcdb,对应哈希表示为1231,假设此时寻找的子串长度为3,首先需要按照进制转化方法计算123的哈希值为
h
1
=
1
∗
2
6
2
+
2
∗
26
+
3
h_1=1*26^2+2*26+3
h1=1∗262+2∗26+3,而需要计算的下一个子串为231,其哈希值为
h
2
=
2
∗
2
6
2
+
3
∗
26
+
1
h_2=2*26^2+3*26+1
h2=2∗262+3∗26+1。
根据滑动窗口原理,
h
2
h_2
h2可通过
h
1
h_1
h1减去头部已滑出窗口范围的
1
∗
2
6
2
1*26^2
1∗262后,剩余部分左移乘以进制数,再加上末尾滑进窗口范围的1即可求得。通过滑动窗口可以大幅减少计算哈希值所消耗的时间。
程序代码
class Solution:
def longestDupSubstring(self, s: str) -> str:
def check(sublen):
num1 = num2 = 0
sl1 = pow(base,sublen-1,mod1)
sl2 = pow(base,sublen-1,mod2)
for i in range(sublen):
num1 = (num1 * base + arr[i]) % mod1
num2 = (num2 * base + arr[i]) % mod2
record = set([(num1,num2)])
for i in range(slen - sublen):
# 滑动窗口
num1 = ((num1-arr[i]*sl1)*base+arr[sublen+i])%mod1
num2 = ((num2-arr[i]*sl2)*base+arr[sublen+i])%mod2
if (num1,num2) in record:
return i+1 # 返回重复子串的起始索引
record.add((num1,num2))
return -1 # 不存在重复子串则返回-1
base, mod1, mod2 = 26, 1313113, 1313137
arr = [ord(x) - ord("a") for x in s]
slen = len(s)
left, right = 0, slen-1
length, start = 0, -1
while left <= right:
mid = (left + right) // 2
index = check(mid)
if index != -1: # 未找到重复子串
length = mid
start = index
left = mid + 1
else:
right = mid - 1
return s[start : start + length]
算法实现过程中遇到的问题
append方法超时
在最初实现时,记录已有哈希的record使用的是List,发现一个不存在的哈希数对时使用append方法将其加入到record列表中。由于append方法是在列表末尾加入元素,每次都要对列表进行遍历,导致时间开销极大,在跑某一组数据时耗时5000+ms,直接超时。
之后将record用set集合表示,发现一个不存在的哈希数对时使用add方法加入set中,上述数据耗时128ms。(大概由于set无序,add方法不用对set进行遍历)
取模导致的非重复子串误判
由于字符串可能相当大,在对哈希结果取模后相同的概率也会较大。
最初只选用一个模数,只能跑过一半的测试样例。尝试增加一个模数,用两个模数取模得到的哈希数对进行判别,能够通过大部分测试样例。最后几个测试样例字符串长度很大,通过多次尝试增大模数最终成功通过全部样例。(可以尝试使用双底数双模数)