题目
给你一个字符串 s ,考虑其所有 重复子串 :即,s 的连续子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。
返回 任意一个 具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 “” 。
解题思路
如果枚举重复子串的长度,用滑动窗口和哈希来判重,必定超时。我们有两个可以优化的地方。
- 第一个是枚举重复子串的长度,对于这个长度,大于这个长度的子串中必定不会出现重复子串,小于这个长度的子串中必定会出现重复字串,其具有二分性,那么这里可以用到二分,这里变成了O( l o g n logn logn)。
- 第二个可以优化的地方是哈希判重,如果直接用
Set<String>
来维护,由于子串存进容器的哈希函数执行与子串的长度有关,复杂度最坏可能达到O( n {n} n),由于枚举滑动窗口的时间复杂度是O( n n n),所以总时间复杂度变成了O( n 2 l o g n n^2logn n2logn),会超时。比较好的做法是用字符串哈希,其基本思想是把字符串转换成P进制数,对于字符串保证每个子串的值唯一。可以看看三叶姐的字符串哈希入门。由此可以把哈希查询的复杂度降为O( 1 1 1),算上之前的时间复杂度为O( n l o g n nlogn nlogn)。
代码
class Solution {
public long[] pos, hash;
int length, P = 13131;
String ans, s;
public String longestDupSubstring(String s) {
char[] chars = s.toCharArray();
init(s);
pos[0] = 1;
for (int i = 1; i <= length; i++) {
pos[i] = pos[i - 1] * P;
hash[i] = hash[i - 1] * P + chars[i - 1] - 'a' + 1;
}
int left = 0, right = length;
while (left < right) {
int mid = left + right + 1 >> 1;
if (check(mid)) left = mid;
else right = mid - 1;
}
return ans;
}
private void init(String s) {
this.s = s;
ans = "";
length = s.length();
pos = new long[length + 50];
hash = new long[length + 50];
}
private boolean check(int mid) {
Set<Long> set = new HashSet<>();
for (int i = 1; i + mid - 1 <= length; i++) {
int j = i + mid - 1;
long now = hash[j] - hash[i - 1] * pos[j - i + 1];
if (set.contains(now)) {
ans = ans.length() > mid ? ans : s.substring(i - 1, j);
return true;
}
set.add(now);
}
return false;
}
}