【综合笔试题】1044. 最长重复子串 : 两种强有力的字符串处理方式

简介: 【综合笔试题】1044. 最长重复子串 : 两种强有力的字符串处理方式

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 1044. 最长重复子串 ,难度为 困难


Tag : 「字符串哈希」、「二分」、「后缀数组」


给你一个字符串 s ,考虑其所有 重复子串 :即 s 的连续子串,在 s 中出现 22 次或更多次。这些出现之间可能存在重叠。


返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 ""


示例 1:


输入:s = "banana"
输出:"ana"
复制代码


示例 2:


输入:s = "abcd"
输出:""
复制代码


提示:


  • 2 <= s.length <= 3 * 10^42<=s.length<=3104
  • s 由小写英文字母组成


字符串哈希 + 二分



题目要求得「能取得最大长度的任一方案」,首先以「最大长度」为分割点的数轴具有「二段性」:


  • 小于等于最大长度方案均存在(考虑在最大长度方案上做删减);
  • 大于最大长度的方案不存在。


二分范围为 [0, n][0,n],关键在于如何 check 函数,即实现「检查某个长度 lenlen 作为最大长度,是否存在合法方案」。


对于常规做法而言,可枚举每个位置作为起点,得到长度为 lenlen 的子串,同时使用 Set<String> 容器记录已被处理过子串有哪些,当容器中出现过当前子串,说明存在合法方案。


但是该做法实现的 check 并非线性,子串的生成和存入容器的时执行的哈希函数执行均和子串长度相关,复杂度是 O(n * len)O(nlen)


我们可以通过「字符串哈希」进行优化,对「字符串哈希」不熟悉的同学可以看前置 🧀 字符串哈希入门


具体的,在二分前先通过 O(n)O(n) 的复杂度预处理出哈希数组,从而确保能够在 check 时能够 O(1)O(1) 得到某个子串的哈希值,最终将 check 的复杂度降为 O(n)O(n)


代码:


class Solution {
    long[] h, p;
    public String longestDupSubstring(String s) {
        int P = 1313131, n = s.length();
        h = new long[n + 10]; p = new long[n + 10];
        p[0] = 1;
        for (int i = 0; i < n; i++) {
            p[i + 1] = p[i] * P;
            h[i + 1] = h[i] * P + s.charAt(i);
        }
        String ans = "";
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            String t = check(s, mid);
            if (t.length() != 0) l = mid;
            else r = mid - 1;
            ans = t.length() > ans.length() ? t : ans;
        }
        return ans;
    }
    String check(String s, int len) {
        int n = s.length();
        Set<Long> set = new HashSet<>();
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            long cur = h[j] - h[i - 1] * p[j - i + 1];
            if (set.contains(cur)) return s.substring(i - 1, j);
            set.add(cur);
        }
        return "";
    }
}
复制代码


  • 时间复杂度:令 nn 为字符串 s 的长度,预处理出哈希数组的复杂度为 O(n)O(n);二分最大长度的复杂度为 O(n\log{n})O(nlogn);整体复杂度为 O(n\log{n})O(nlogn)
  • 空间复杂度:O(n)O(n)


后缀数组



另外一个较为进阶的做法是使用「后缀数组」,后缀数组有基于基数排序的倍增实现,复杂度为 O(n\log{n})O(nlogn),也有基于 DC3O(n)O(n) 做法。


DC3 的做法已经写不出来了,写一个基于「基数排序」的倍增做法。


后缀数组包含几个核心数组:


  • sa 数组:字典序排名为 ii 的后缀是第几个;
  • rk 数组:第 ii 个后缀的排名是多少(不难发现 rksa 满足 sa[rk[i]] = rk[sa[i]] = isa[rk[i]]=rk[sa[i]]=i);
  • height 数组:存储 sa[i]sa[i]sa[i - 1]sa[i1]LCP(最长公共前缀) 为何值。


代码:


class Solution {
    int N = 30010;
    int[] x = new int[N], y = new int[N], c = new int[N];
    int[] sa = new int[N], rk = new int[N], height = new int[N];
    void swap(int[] a, int[] b) {
        int n = a.length;
        int[] c = a.clone();
        for (int i = 0; i < n; i++) a[i] = b[i];
        for (int i = 0; i < n; i++) b[i] = c[i];
    }
    public String longestDupSubstring(String s) {
        int n = s.length(), m = 128;
        s = " " + s;
        char[] cs = s.toCharArray();
        // sa:两遍基数排序,板子背不下来也可以直接使用 sort,复杂度退化到 n \log^2 n
        for (int i = 1; i <= n; i++) {
            x[i] = cs[i]; c[x[i]]++;
        }
        for (int i = 2; i <= m; i++) c[i] += c[i - 1];
        for (int i = n; i > 0; i--) sa[c[x[i]]--] = i;
        for (int k = 1; k <= n; k <<= 1) {
            int cur = 0;
            for (int i = n - k + 1; i <= n; i++) y[++cur] = i;
            for (int i = 1; i <= n; i ++) {
                if (sa[i] > k) y[++cur] = sa[i] - k;
            }
            for (int i = 1; i <= m; i++) c[i] = 0;
            for (int i = 1; i <= n; i++) c[x[i]]++;
            for (int i = 2; i <= m; i++) c[i] += c[i - 1];
            for (int i = n; i > 0; i--) {
                sa[c[x[y[i]]]--] = y[i];
                y[i] = 0;
            }
            swap(x, y);
            x[sa[1]] = cur = 1;
            for (int i = 2; i <= n; i++) {
                if (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = cur;
                else x[sa[i]] = ++cur;
            }
            if (cur == n) break;
            m = cur;
        }
        // height
        int k = 0;
        for (int i = 1; i <= n; i ++) rk[sa[i]] = i;
        for (int i = 1; i <= n; i++) {
            if (rk[i] == 1) continue;
            int j = sa[rk[i] - 1];
            k = k > 0 ? k - 1 : k;
            while (i + k <= n && j + k <= n && cs[i + k] == cs[j + k]) k++;
            height[rk[i]] = k;
        }
        int idx = -1, max = 0;
        for (int i = 1; i <= n; i++) {
            if (height[i] > max) {
                max = height[i];
                idx = sa[i];
            }
        }
        return max == 0 ? "" : s.substring(idx, idx + max);
    }
}
复制代码


  • 时间复杂度:O(n\log{n})O(nlogn)
  • 空间复杂度:O(n)O(n)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.1044 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
4月前
|
存储 算法 程序员
C语言:基础与应用的双重魅力
C语言:基础与应用的双重魅力
|
2月前
|
C语言
C语言100道基础拔高题(2)
【7月更文挑战第26天】
28 2
|
2月前
|
存储 算法 索引
深度挖掘:Python并查集背后的秘密,让你的代码逻辑清晰如水晶!
【7月更文挑战第17天】并查集,一种高效处理集合合并与查询的数据结构,常用于图论、社交网络分析等。Python中的实现利用数组存储元素的父节点,通过路径压缩和按秩合并优化查找和合并操作。简单代码示例展示了查找和合并方法,以及应用在检测无向图环路。并查集以其优雅的解决方案在算法世界中闪耀,提升代码的清晰度和效率。
40 5
|
3月前
|
运维 程序员
程序员在企业中是如何做需求的
需求从哪里来,到哪里去
20 0
程序员在企业中是如何做需求的
|
2月前
|
机器学习/深度学习 开发框架 数据可视化
我们可以从系统工程的角度来讨论如何优化组织架构,并给出一些可能涉及的Python应用领域的示例。
我们可以从系统工程的角度来讨论如何优化组织架构,并给出一些可能涉及的Python应用领域的示例。
|
4月前
|
对象存储 C++ 索引
C++ 字符串操作的技术性探讨
C++ 字符串操作的技术性探讨
16 1
|
4月前
|
网络协议 编译器 数据处理
C语言:其独特的魅力、广泛的应用领域与深入的代码实践
C语言以其简洁语法和高效性能在编程世界中闪耀,适用于系统编程、嵌入式、游戏开发和网络通信等领域。其独特魅力包括底层访问能力、高效稳定、灵活数据结构和良好可移植性。通过代码示例展示其基本语法和功能,C语言是理解计算机科学和创建高性能程序的关键工具。
|
4月前
|
Rust 安全 前端开发
Rust还是其他语言:考量因素与案例分析
【2月更文挑战第1天】本文将探讨在选择编程语言时,为什么Rust可能会成为理想的选择。我们将分析Rust的主要优势,如内存安全、性能、并发编程和所有权系统,并将其与其他流行的编程语言进行比较。此外,我们还将通过具体的案例分析,展示Rust在实际应用中的优势和应用场景。
|
C语言
【C语言】结构体解谜:拆解数据的力量!
【C语言】结构体解谜:拆解数据的力量!
70 0
|
4月前
|
存储 C语言
深入浅出 C 语言:学变量、掌控流程、玩指针,全方位掌握 C 编程技能
C 语言介绍 C 语言的特性 C 语言相对于其他语言的优势 C 程序的编译 C 中的 Hello World 程序
69 2