[LeetCode] Longest Duplicate Substring

倖福魔咒の 提交于 2020-01-18 03:30:29

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官方的题解:

https://leetcode-cn.com/problems/longest-duplicate-substring/solution/zui-chang-zhong-fu-zi-chuan-by-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;
    }
};

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!