在问 · 算法题解

1044. 最长重复子串

困难二分查找二分长度 + 滚动哈希力扣原题 ↗

题目描述

输入字符串 s。如果 s 的某个连续子串在 s 中至少出现两次,就称它为重复子串。两次出现的位置允许重叠。

返回长度最长的重复子串。有多个长度相同的答案时,返回其中任意一个。如果不存在重复子串,返回空串 ""。

示例 1
输入:s = "banana"
输出:"ana"

解释:ana 在下标 1 和 3 处各出现一次,两处有重叠。

示例 2
输入:s = "abcd"
输出:""

解释:四个字母各不相同,没有重复子串。

数据范围

  • 2 <= s.length <= 3 * 10^4
  • s 只含小写英文字母。

解题思路:二分长度 + 滚动哈希

如果长度 L 的子串有重复,那么更短的长度也一定有重复(取它的前缀即可),所以可以对长度 L 二分。判断“长度 L 是否有重复”时,用 Rabin-Karp 滚动哈希:窗口右移一格,哈希值去掉最左字符、加入新字符,O(1) 更新;哈希相同的再逐字比较,排除冲突。

为什么正确

二分过程中,长度 < lo 的都已确认有重复(ans 记录其中最长的一个),长度 > hi 的都已确认没有重复。

复杂度分析

时间 O(n log n)(期望),空间 O(n)。

易错点

哈希相等不代表子串相等,必须逐字确认或使用双哈希。减去最左字符后取模可能为负,C++/Java/Go/JS 要加 M 再取模。乘法 h*B 和 c*B^L 需要 64 位整数。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def longestDupSubstring(self, s: str) -> str:
        n, B, M = len(s), 131, 1000000007
        def dup(L):
            p = pow(B, L, M)
            h, seen = 0, {}
            for i in range(n):
                h = (h * B + ord(s[i])) % M
                if i >= L:
                    h = (h - ord(s[i - L]) * p) % M
                if i >= L - 1:
                    st = i - L + 1
                    for j in seen.get(h, []):
                        if s[j:j + L] == s[st:st + L]:
                            return st
                    seen.setdefault(h, []).append(st)
            return -1
        lo, hi, ans = 1, n - 1, ""
        while lo <= hi:
            mid = (lo + hi) // 2
            st = dup(mid)
            if st >= 0:
                ans, lo = s[st:st + mid], mid + 1
            else:
                hi = mid - 1
        return ans

载入编辑器,自己跑一遍 →

其他解法

二分哈希利用“长度 L 有重复则更短也有重复”的单调性,代码较短但依赖哈希(本题主解法碰到哈希相同时会逐字符核对);后缀数组一次构造后直接读出最大 LCP,是确定性算法,还能解决不同子串计数等更多问题。

解法 2:后缀数组 + LCP 时间 O(n log² n),空间 O(n)

任何重复子串都是两个不同后缀的公共前缀。把所有后缀按字典序排序得到后缀数组,那么最长公共前缀一定出现在字典序相邻的两个后缀之间。后缀数组用倍增法构造:第 k 轮用前一轮长度 k 的排名对 (rank[i], rank[i+k]) 排序,得到长度 2k 的排名,直到所有排名互不相同。再用 Kasai 算法按原串顺序求每个后缀与其字典序前一个后缀的公共前缀长度,利用“下一个位置的 LCP 至少为当前减 1”的性质在 O(n) 内算完。最大的 LCP 对应的前缀就是答案。

结果确定,没有哈希冲突的风险,而且后缀数组是处理字符串重复类问题的通用工具。实现比二分哈希复杂,倍增法用比较排序时多一个 log 因子,换成基数排序可以降到 O(n log n)。

class Solution:
    def longestDupSubstring(self, s: str) -> str:
        n = len(s)
        base = max(n, 256) + 2
        rank = [ord(c) for c in s]
        sa = list(range(n))
        k = 1
        while True:
            key = [rank[i] * base + (rank[i + k] + 1 if i + k < n else 0) for i in range(n)]
            sa.sort(key=key.__getitem__)
            nr = [0] * n
            for j in range(1, n):
                nr[sa[j]] = nr[sa[j - 1]] + (key[sa[j]] != key[sa[j - 1]])
            rank = nr
            if rank[sa[-1]] == n - 1:
                break
            k <<= 1
        best = pos = h = 0
        for i in range(n):
            if rank[i] == 0:
                h = 0
                continue
            j = sa[rank[i] - 1]
            while i + h < n and j + h < n and s[i + h] == s[j + h]:
                h += 1
            if h > best:
                best, pos = h, i
            if h:
                h -= 1
        return s[pos:pos + best]

二分查找 专题的其他题目

查看 二分查找 全部题目 →