Given a string S, consider all duplicated substrings: (contiguous) substrings of S that occur 2 or more times. (The occurrences may overlap.)
Return any duplicated substring that has the longest possible length. (If S does not have a duplicated substring, the answer is "".)
Example 1:
Input: "banana"
Output: "ana"
Example 2:
Input: "abcd"
Output: ""
Note:
2 <= S.length <= 10^5
S consists of lowercase English letters.
来自leetcode官方的题解:
方法一:二分查找 + Rabin-Karp 字符串编码
分析
我们可以把这个问题分解成两个子问题:
从 1 到 N 中选取子串的长度 L;
检查字符串中是否存在长度为 L 的重复子串。
子任务一:二分查找
解决子问题一的最简单的方法是,我们从 L = N - 1 开始,依次递减选取子串的长度,并进行判断。如果发现存在长度为 L 的重复子串,就表示 L 是最长的可能长度。
但我们发现,如果字符串中存在长度为 L 的重复子串,那么一定存在长度为 L0 < L 的重复子串(选取长度为 L 的重复子串的某个长度为 L0 的子串即可),因此我们可以使用二分查找的方法,找到最大的 L。
子任务二:Rabin-Karp 字符串编码
我们可以使用 Rabin-Karp 算法将字符串进行编码,这样只要有两个编码相同,就说明存在重复子串。对于选取的长度 L:
使用长度为 L 的滑动窗口在长度为 N 的字符串上从左向右滑动;
检查当前处于滑动窗口中的子串的编码是否已经出现过(用一个集合存储已经出现过的编码);
若已经出现过,就说明找到了长度为 L 的重复子串;
若没有出现过,就把当前子串的编码加入到集合中。
可以看出,Rabin-Karp 字符串编码的本质是对字符串进行哈希,将字符串之间的比较转化为编码之间的比较。接下来的问题是,在滑动窗口从左向右滑动时,如何快速计算出当前子串的编码呢?如果需要在 O(L)O(L) 的时间内算出编码,这种方法就没有意义了,因为这个直接进行字符串比较需要的时间相同。
为了能够快速计算出字符串编码,我们可以将字符串看成一个 26 进制的数(因为字符串中仅包含小写字母),它对应的 10 进制的值就是字符串的编码值。首先将字符转换为 26 进制中的 0-25,即通过 arr[i] = (int)S.charAt(i) - (int)'a',可以将字符串 abcd 转换为 [0, 1, 2, 3],它对应的 10 进制值为:
我们将这个表达式写得更通用一些,设 c_ici 为字符串中第 i
个字符对应的数字,a = 26a=26 为字符串的进制,那么有:
当我们向右移动滑动窗口时,例如从 abcd 变成 bcde,此时字符串对应的值从 [0, 1, 2, 3] 变成 [1, 2, 3, 4],移除了最高位的 0,并且在最低位添加了 4,那么我们可以快速地计算出新的字符串的编码:
更加通用的写法是:
这样,我们只需要 O(L)O(L) 的时间复杂度计算出最左侧子串的编码,这个 O(L)O(L) 和滑动窗口的循环是独立的。在滑动窗口向右滑动时,计算新的子串的编码的时间复杂度仅为 O(1)O(1)。
最后一个需要解决的问题是,在实际的编码计算中,a^La
L
的值可能会非常大,在 C++ 和 Java 语言中,会导致整数的上溢出。一般的解决方法时,对编码值进行取模,将编码控制在一定的范围内,防止溢出,即h = h % modulus。根据 生日悖论,模数一般需要和被编码的信息数量的平方根的数量级相同,在本题中,相同长度的子串的数量不会超过 NN 个,因此选取模数是 2^{32}2
32
(无符号整型数的最大值)是足够的。在 Java 中可以用如下的代码实现取模:
h = (h * a - nums[start - 1] * aL % modulus + modulus) % modulus;
h = (h + nums[start + L - 1]) % modulus;
而在 Python 中,整型数没有最大值,因此可以在运算的最后再取模:
h = (h * a - nums[start - 1] * aL + nums[start + L - 1]) % modulus
在解决算法题时,我们只要判断两个编码是否相同,就表示它们对应的字符串是否相同。但在实际的应用场景中,会出现字符串不同但编码相同的情况,因此在实际场景中使用 Rabin-Karp 字符串编码时,推荐在编码相同时再对字符串进行比较,防止出现错误。
class Solution {
private:
int start = 0;
int maxLen = 0;
int helper(vector<int> &table,int strLen,int subLen)
{
unsigned int a =1;
unsigned int temp = 0;
unordered_set<unsigned int> mm;
for(int i = 0;i < subLen;i++)
{
a = a*26 % UINT_MAX;
temp = temp*26 % UINT_MAX;
temp += table[i];
}
mm.insert(temp);
for(int i = 1;i < (strLen -subLen + 1);i++)
{
temp = temp*26 % UINT_MAX - table[i-1]*a % UINT_MAX + table[i + subLen -1];
if(mm.find(temp) != mm.end())
{
if(subLen > maxLen)
{
start = i;
maxLen = max(maxLen,subLen);
}
return true;
}
mm.insert(temp);
}
return false;
}
public:
string longestDupSubstring(string S) {
if(S.empty()) return "";
int n = S.size();
int left = 1,right = n;//left = 1???
vector<int> table;
string res = "";
int subLen = 0;
for(int i = 0;i < n;i++)
{
table.push_back(S[i] - 'a');
}
while(left < right)
{
subLen = (left + right) / 2;
if(helper(table,n,subLen))
{
left = subLen + 1;
}
else
right = subLen;
}
if(maxLen != 0)
res = S.substr(start,maxLen);
return res;
}
};
来源:CSDN
作者:Lei_lz
链接:https://blog.csdn.net/weixin_37992828/article/details/103864187