面试算法-子串问题

107 阅读23分钟
  1. 最长相同子串

todo: dp 二分 + 字符串hash,做法和1044基本一样。

  1. 最长重复子串

leetcode1044 字符串hash(可以看成另类的前缀和), 后缀数组(TODO), 倍增后缀数组(n->logn)

  1. 模式串匹配

leetcode28 暴力, KMP

  1. 最长回文子串

leetcode5 dp, 中心扩散, manacher算法

  1. 回文子串变体

leetcode 1542, 暴力, 位运算压缩

重要的事情说三遍,子串一般都可以用前缀优化,前缀和子串的关系如下

前缀i - 前缀j = 子串[i+1:j+1]

前缀i - 前缀j = 子串[i+1:j+1]

前缀i - 前缀j = 子串[i+1:j+1]


子串的枚举可以用 `[i:j] for i in range(n) for j in range(i, n)

也可以用 [i, len][i: i + len - 1] for len in range(n) for i in range(len_s - len ) 前闭后闭写法

子串的枚举可以用 `[i:j] for i in range(n) for j in range(i, n)

也可以用 [i, len][i: i + len - 1] for len in range(n) for i in range(len_s - len ) 前闭后闭写法

子串的枚举可以用 `[i:j] for i in range(n) for j in range(i, n)

也可以用 [i, len][i: i + len - 1] for len in range(n) for i in range(len_s - len ) 前闭后闭写法

子串是连续的,所以如果求 最长,最短子串,可以考虑用二分这样的做法应该用i, len的方式表示字串

e.g 如果求最长子串,那么如果找到了len1符合,那么<=len1的一定也符合,如果找到了len2不符合,那么 >= len2的一定也不符合

子串是连续的,所以如果求 最长,最短子串,可以考虑用二分这样的做法应该用i, len的方式表示字串

e.g 如果求最长子串,那么如果找到了len1符合,那么<=len1的一定也符合,如果找到了len2不符合,那么 >= len2的一定也不符合

子串是连续的,所以如果求 最长,最短子串,可以考虑用二分这样的做法应该用i, len的方式表示字串

e.g 如果求最长子串,那么如果找到了len1符合,那么<=len1的一定也符合,如果找到了len2不符合,那么 >= len2的一定也不符合

最长相同子串

最长重复子串

最长回文子串

dp做法

中心扩展算法

Manacher算法(todo) zhuanlan.zhihu.com/p/70532099

常见的匹配串算法 KMP,

KMP 算法详解 - 知乎 (zhihu.com) www.cnblogs.com/labuladong/…

注意,子串问题的思考方式不能是 "选or不选",这样就表示子序列了。因为选或不选的含义是,最终序列在原序列里可以是不连续的。

子串问题的思考方式是 "要么从头再来,要么从上一次转移而来"。

子串问题可以用"前缀"的思考方式来看,因为不同于子序列问题,子序列问题的最优解永远出自 str[:len (str)],而 子串问题的最优解可能出自任何str构成的前缀str[:i] i <= len(str) 中。

证明:如果子序列问题的最优解出自str[:i] 其中i < len(str),那么可以通过str[:j] j = i + 1同时不在原串中选择str[j]得到和str[:i]一样的结果,以此类推,也就是最优解一定出自str[:len(str)]中。

但是对于子串问题,如果最优解出自str[:i],那么能不能推出有 优于最优解的结果出自str[:j],其中j > i呢?

  • 如果可以选str[j],那么是显然的
  • 如果不能选str[j],那么str[:j] 不能从str[:i]的结果转移而来。

因此,最优解可能出自任何前缀中。

时间复杂度O(n^2),一共有O(n2)O(n^2)个状态,每个状态转移需要O(1)的复杂度

1. 最长回文子串

5. 最长回文子串

下面的改成dp就能过了。写成记忆化搜索,主要是可以清楚的看出我们在i == j以及i + 1 == j时的特殊处理,和子串长度 >= 3时的通用处理。

class Solution:
    def longestPalindrome(self, s: str) -> str:

        max_len = 0
        ans = ""
        n = len(s)
        ans_i = 0
        ans_j = 0
        @cache
        def dfs(s: str, i: int, j: int):
            # 转移方程 s[i][j] = s[i+1][j-1] and s[i] == s[j] 只在>=3的子串中可用,对于长度为1或者2的子串需要自行处理。
            if i == j:
                return True
            if i + 1 == j:
                return s[i] == s[j]
            
            if s[i] == s[j] and dfs(s, i+1, j-1):
                # 这里要理解子串问题的精髓: 如果不能让结果连续,那直接就退出了。
                return True

            return False

        for i in range(0, n):
            for j in range(i, n):
                if dfs(s, i, j):
                    if j - i + 1 > max_len:
                        max_len = j - i + 1
                        ans_i = i
                        ans_j = j

        return s[ans_i: ans_j + 1]

中心扩展算法

任何涉及到模拟的做法,尽量化简需要模拟的维度会让问题更好思考。

比如本题,如果用模拟来做,无非就是枚举所有子串区间里的情况,两个维度i和j,满足约束j >= i

如果可以化简的话,就比较舒服了。思考子串问题,我们习惯于 从以某个字符为end思考,那可不可换一个角度,以某个字符作为center思考?

这里需要注意下spread_center的语义:

最终返回的(l, r)是前开后开的,因为如果s[l] != s[r]直接走人了。对于区间开闭关系和区间内元素的个数必须了然于心。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        
        n = len(s)
        max_len = 0
        ans_i = 0
        ans_j = 0
        
        
        # 从该中心开始扩散
        # 难点: 如果以两个字符为中心呢? 无所谓,我们可以把过程做的更通用
        for i in range(n):
            # 这里的语义是(l, r) 前开后开啊
            l, r = self.center_spread(s, i, i)
            if r - l - 1 > max_len:
                max_len = r - l - 1
                ans_i = l
                ans_j = r

        for i in range(n-1):
            # 两个中心扩散
            l, r = self.center_spread(s, i, i + 1)
            if r - l - 1 > max_len:
                max_len = r - l - 1
                ans_i = l
                ans_j = r
        
        return s[ans_i+1: ans_j]

    def center_spread(self, s, left, right):
        L = left
        R = right

        while L >= 0 and R < len(s) and s[L] == s[R]:
            L -= 1
            R += 1
        
        return L, R

Manacher算法

又是学了忘忘了学系列。

zhuanlan.zhihu.com/p/70532099


超赞字符串(leetcode1542)

1542. 找出最长的超赞子字符串

反正子串好几种变体,取决于题目咋定义。

暴力做法

枚举所有区间,判断区间内是不是超赞字符串。

这个超赞字符串的约束不如 回文串,基于频率统计即可。

写完之后,我们发现,

  1. 每个区间都需要维护词频信息,能不能弄出某个数据结构,使得query一个区间内词频的复杂度下降为O(1) ?
class Solution:
    def longestAwesome(self, s: str) -> int:

        # 直观来想,相比严格回文,这道题的约束少了很多。只需要子串s[i:j] 里的字符词频,都是偶数,或者仅有一个是奇数即可。
        # 这里难度在于 1. 如何构造一个 "能区间query的词频数据结构",使得我们query[L:R] 可以快速得到区间的词频表。
        # 2. 如何快速的统计所有词频中,是不是只有一个奇数,其他都是偶数,或者全都是偶数?
        max_len = 0
        ans_i = 0
        ans_j = 0
        # brute force!!!
        n = len(s)
        # O(n^2)
        for i in range(0, n):
            for j in range(i, n):
                # O(n)
                if self.isAwesome(s, i, j):
                    if j - i + 1 > max_len:
                        max_len = j - i + 1
                        ans_i = i
                        ans_j = j
        return max_len

    
    def isAwesome(self, s, left, right):
        m = {}
        for i in range(left, right + 1):
            if s[i] not in m:
                m[s[i]] = 1
            else:
                m[s[i]] += 1
        even = 0
        # print(m)
        for v in m.values():
            if v % 2:
                even += 1
        return even == 0 or even == 1

前缀异或和

leetcode.cn/problems/si…

前缀的变体

刚做的时候,其实想到了"前缀数组"这个,我估计是之前做过上面那道题,"只出现一次的数字",这个题其实和这个思想差不多。

超赞字符串的特性是:基于词频求解。而且词频还要求只能存在一个奇数 or 没有奇数

考虑到前缀异或和的性质

sum[i] = arr[0] ^ arr[1] ^ ... ^arr[i]

sum[i] ^ sum[j] = arr[0] ^ ... ^ arr[i] ^ arr[0] ^ ... ^ arr[i] ^ ... ^ arr[j] = arr[i+1] ^ arr[i+2] ^ ... ^ arr[j]

注意异或操作的特性, a ^ a = 0, 0 ^ b = b

O(n^2A) 还是过不了。 注意这里用到了 经典的二进制操作判断二进制串有几个1。设计上,我们用一个10bit的串,代表对应num出现的频率 % 2。

class Solution:
    def longestAwesome(self, s: str) -> int:

        # 用一个 10bit的二进制串,记录0-9这几个数字是奇数还是偶数
        # 即出现频率 % 2 
        # 异或的特性:0变1,1变0,等价于不进位加法。可以满足我们判断奇偶的目的

        D = 10
        digit = 0
        max_len = 0
        n = len(s)
        pre = [0] * len(s)
        for i, x in enumerate(s):
            digit = 1 << int(x)
            if i == 0:
                pre[0] = digit
            else:
                pre[i] = pre[i-1] ^ digit

        # print([bin(x) for x in pre])

        for idx, x in enumerate(pre):
            if self.check(x) <= 1:
                # print(bin(x), self.check(x))
                max_len = max(max_len, idx + 1)

        for i in range(0, n):
            for j in range(i, n):
                if self.check(pre[j] ^ pre[i]) <= 1:
                    # 奇数的频率 <= 1
                    # pre[j] ^ pre[i] 求的区间[i+1:j]的每一位数的频率
                    max_len = max(max_len, j - i)
                    # print(bin(pre[j] ^ pre[i]), self.check(pre[j] ^ pre[i]), i, j, max_len)
        return max_len



    # O(A) 复杂度,检验digit上有多少个1
    def check(self, digit):
        ans = 0
        d = digit
        
        while digit != 0:
            ans += digit & 1
            digit >>= 1

        
        return ans

第一次打这么富裕的仗,开了一个O(2D)O(2^D)的数组。

f[pre] = i表示前缀s[:i]的异或和为pre。

关键性质: 如果f[pre] = i 同时遍历到j的时候,计算的前缀异或和还是pre,说明s[i+1:j+1]可以构成Awesome串

性质2: 如果f[pre ^ (1 << d)] = i,遍历到j时,前缀疑惑和为pre,则说明只有d一个数字奇偶关系对不上,此时也满足要求

边界条件pre[0] = -1,子串问题需要对0单独处理,因为pre[j] - pre[0] 其实表示s[1:j+1],我们如果想表示s[0:j+1],那就得用pre[j]。由于题目中都是用当前的前缀和,减去前面的前缀和,而没有对"仅当前的前缀和"进行处理,这是因为pre[0]被初始化为-1。

如果不理解,可以看下上面的解法,如果不这么做,就得额外对单纯的pre[j]再遍历一次。


class Solution:
    def longestAwesome(self, s: str) -> int:

        D = 10
        n = len(s)

        # 空间复杂度这么高?
        pos = [n] * (1 << D)

        pos[0] = -1
        ans = pre = 0

        for i, x in enumerate(map(int, s)):

            # 前缀和问题的通用优化,由于我们只关心最后一次的结果,所以中间过程可以边计算边处理
            # pre: 目前前缀s[:i] 的前缀异或和
            pre ^= 1 << x

            # 这里有点dp转移那味道了?
            ans = max(
                ans, i - pos[pre],  # i - pos[pre] 注意pos[pre] 代表上一轮的pre所在的前缀[:i_old],这里的含义是,前缀[:i]和[:i_old]的异或和一样,不就是说子串[i_old + 1: i]异或和为0,自然能构成结果

                # i - pos[pre * (1 << d)] 的含义也很好理解了,和前缀pre[:i_old] **只有一个**整数存在奇偶差异,这样的子串s[i+1:j+1]也可以到达Awesome串
                max(i - pos[pre ^ (1 << d)] for d in range(D))
            )
            if pos[pre] == n:
                pos[pre] = i
        return ans
        

实现strStr

leetcode28

经典求解A是不是B的子串的问题

brute-force

  • 枚举所有子串[i:j],判断该子串是否在s[i+1:]后出现过。注意题目说子串可以重复,因此从s[i]之后的所有位置都可以作为判断后面是否存在连续子串的选择。
class Solution:
    def longestDupSubstring(self, s: str) -> str:
        # 暴力做法 O(n^3)
        n = len(s)
        ans = ""
        for i in range(n):
            for j in range(i, n):
                # 看下子串i, j 是否有在 后面出现过
                if s[i:j] in s[i+1:]:
                    if len(s[i:j]) > len(ans):
                        ans = s[i:j]
        return ans

  • 对于这个问题,我们可以从子串优化,也可以从 "最大"优化。
  • 对子串的优化,我们见过很多次了,无非就是前缀数组/后缀数组 + 预计算。
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:

        # 题目中有 needle长度比haystack还大的case,无语
        if len(haystack) < len(needle):
            return -1
        
        # 暴力做法:
        # 尝试以 word1[i] 为起点开始匹配模式串pat,如果匹配失败,则从word[i+1] 为起点重新匹配模式串

        # O(n) 尝试以word[i]为起点
        for i in range(len(haystack)):
            idx = i
            # O(m) 匹配needle
            for m, x in enumerate(needle):
                if idx >= len(haystack):
                    # 此时有当前位置上的i都没法和原串匹配了,再往后试肯定更没有了
                    return -1
                if haystack[idx] == x:
                    idx += 1
                else:
                    break
            
            # 如果匹配成功,退出时idx一定有 idx = i + len(m)
            if idx == i + len(needle):
                return i
        return -1

这里有几个问题

  1. 如果模式串没有字符c,但是原串里匹配到了c。比如

word: ababdabacb pat: ababc

暴力匹配法

ababdabacb
ababc <=
 _    <=
  ab
   _  <=
    _ <=
     ab

可以看到,尽管我们知道pat里没有d,但是为了从d走过去,以暴力的做法要走好几步。

  1. 如果目前串和pat存在一个前缀,此时发生了不匹配,如何快速走过这一部分

word: aaaaaaaaaaaacccb pat: accb

aaaaaaaaaaaacccb
a
 _
  _ 
   _
    _
     _
      ....
           acc

所以,针对这些情况 优化点只和pat有关,和原串无关

对于任何位置,理论上可以快速找到下一次匹配开始的位置,而且这个位置和原串无关。

这种问题叫做 dfa(deterministic finite automaton)。

dfa的思想是

从当前状态S,接受一个输入I,根据自动机转移规则P,可以得到下一个状态S2

  • 这里状态可以理解为 "匹配到模式串的位置",比如pat[1],其实表示 匹配到模式串的第1个位置这个状态。
  • 初始态的含义是还没开始匹配,终态表示能够成功匹配,所以本题就可以规约为遍历原串的每一位作为状态机的输入,如果能让状态从0走到终态,则问题有解。解为使得问题走向终态时的输入下标

为什么比较难理解?

我们正常的思路应该是 “用模式串比对原串”,但KMP的理念是:“用原串作为模式串状态机的输入”。前者需要对原串的每个位置进行比对,复杂度为O(NM), 后者只需要遍历一次原串作为dfa的输入,复杂度为O(N)

KMP算法的核心

  • 根据pat构造dfa。
  • 遍历原串的每一个char作为dfa的输入
  • 如果可以走向终态,则问题有解

难点:构建dfa

dfa[s1][c] = s2 表示状态S1遇到输入为字符c的时候,会走向状态S2

对于ababc这个串,我们如何理解

  1. 定义初态为"",只有首个字符能进入下一个状态,其他字符都会在初态轮转
  2. 对于状态S1,只有输入为下一个字符时,才会使得状态前进,否则只能后退。
  3. KMP的精髓在于:后退时,不会像暴力匹配那样一次性全部后退,而是找到当前状态构成子串的前缀,并从这里后退
ababc:

如果当前状态为 s3
 a  b  a  b  c
s0 s1  s2 s3 s4
  1. 此时输入为C:那么前进到状态s4 dfa[s3]['c'] = s4 此时有解。

  2. 此时输入为Z: 那么直接后退到s0,dfa[s3]['z'] = s0,因为z不在pat中,所以遇到z只能从头匹配。(对应暴力做法中,pat的字符不在原串时的优化)

  3. 此时输入为A,那么后退到S2,dfa[s3]['a'] = s2,此时我们发现,aba这个串在s3状态之前已经判断完了,而且输入有构成了一个aba,所以不如就着之前的来,而不是从头开始。考虑pat为aaab,原串为aaaaaaaab,那么这样的回退只需要会退一小步,非常节约时间。

依旧是学了忘,忘了学系列。

注意下吧,状态有M+1个。0为初态。dp数组只需要初始化 初态遇到pat[0]转移为1即可。

时刻维护pat[:j]的前缀串pat[:X], pat[:j] 遇到非法状态,会回退到pat[:X]。

对于dp[j][c] ,只有 c == pat[j] 时才会让状态推进,否则回退到旧状态dp[X][c]

class KMP {
    int[][] dfa;
    String pat;
    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        // 构建dfa
        dfa = new int[M][256];  // dfa[S1][c] = S2 表示状态S1如果输入c,会转移到S2

        dfa[0][pat.charAt(0)] = 1;  // 对于初始状态0,只有首个字母会进入初始状态1,其他输入都是在初始状态重试
        int X = 0;  // 状态X表示 目前状态j构成的子串str[:j],str[:X]是str[:j]的前缀串
        // 从状态1构造dfa
        for (int j = 1; j < M; ++j) {
            for (int c = 0; c < 256; ++c) {
                dfa[j][c] = dfa[X][c];
            }
            // 只有精准匹配才能进入下一个状态,否则向dfa[x][c]询问回滚后的位置
            dfa[j][pat.charAt(j)] = j + 1;
            X = dfa[X][pat.charAt(j)]; // 尝试从X开始找到当前的字符
        }
    }
    public int trans(int curState, int input) {
        return dfa[curState][input];
    }

    public int[][] getDfa() {
        return dfa;
    }
}

class Solution {
    public int strStr(String haystack, String needle) {
        KMP kmp = new KMP(needle);
        int cur = 0;
        int M = needle.length();
        int i = 0;
        int[][] dp = kmp.getDfa();
        for (char c : haystack.toCharArray()) {
            cur = dp[cur][c];

            // 如果能到达状态M, M为模式串长度,则有解
            if (cur == M) {
                return i - M + 1;
            }
            i++;
        }
        return -1;
    }
}

最长重复子串

首先,子串问题是前缀问题的一种,用前缀法思考子串比较想到优化点。

其次,子串问题又是字符串问题,题目问法的多样性,需要我们把字符串处理为不同的数据结构,问题转换为在这些数据结构上进行优化的方案。

之所以比较难,因为一个问法可能就对应一个专门的数据结构,通常没见过就不会做。

甚至字符串由于其独有特性,比如模式串匹配的特性,回文串的特性,往往还有专门的算法,使得整个体系变得很庞大

  1. 暴力法

看到子串问题,尽管我们可以想到前缀,后缀,因为前缀j - 前缀i其实就是[i+1, j]这个子串。

那么,我们还可以定义子串为 [i, len], i表示起始下标,len表示长度,由于字串连续,知道起始下标i和长度len就能确定唯一的子串。

下面这个做法就是利用了这一点。

  1. 尝试以任何字符为起点
  2. 这个思路比较巧妙 反正是找最长的子串,肯定尝试从子串为1开始寻找,慢慢增加。这个就有点打擂台的意思了。
  3. 连续出现两次就算是连续子串,因此我们只需要一次in就能判断[i, len] 这个子串是不是连续子串了。

证明:有没有可能 [i, len1] 不是连续子串,但是[i, len2]是连续子串,并且len1 < len2。给出这个case,主要是因为考虑有没有一种可能 "如果[i, len1][i+1:] 里不存在,就直接退出,但是[i, len2] 的确在[i+1:]中。

答案是不可能,很显然如果[i, len2] 是更优的解,那么[i, len1] 肯定也出现在[i+1:]中。


class Solution:
    def longestDupSubstring(self, s: str) -> str:
        
        # 假设最长重复子串的长度为1
        j = 1
        ans = ""
        # 尝试以i为重复串的开头
        for i in range(len(s)):
            while s[i:i+j] in s[i+1:]:
                ans = s[i:i+j]
                j += 1

        return ans

在这个方法中,我们按照i, len 对子串进行了枚举,同时使用字符串hash前缀进行了优化,复杂度降低到O(n^2)

class Solution:
    def longestDupSubstring(self, s: str) -> str:

        n = len(s)
        h = [0] * n
        p = [0] * n
        p[0] = 1
        h[0] = ord(s[0])  
        P = 1313131  # P决定了hash冲突的概率

        # 相比一般的前缀数组,需要我们维护一个p,表示字符串每一位的贡献
        # 我们定义串的hash值为 h[i] = s[i] + s[i-1] * p + s[i-2] * p^2 + ... + s[0] * p^(i-1)
        # 这么定义的好处是,如果我们想求s[i:j]的hash值,为 s[i:j] = s[j] + s[j-1] * p + ... + s[i] * p^(j-i)
        # 注意s[i:j] 表示前闭后闭,此时有n = j - i + 1个元素,最后一个元素的指数为 n-1, 所以得出j - i
        # 这个表达式刚好为 s[j] - s[i-1] * p^(j-i+1)
        # s[j] = s[j] + ... + s[0] * p^(j-1)
        # s[i-1] = s[i-1] + ... + s[0] * p^(i-2) (此时补偿一个 j - i + 1), 得到 i-2 + (j-i+1) = j-1

        # 所以,记住结论吧 s[i:j] 前闭后闭的hash值为,pre[j] - pre[i] * p[j - i + 1]

        for i in range(1, n):
            # 注意,字符串hash是一个int
            p[i] = p[i-1] * P
            h[i] = h[i - 1] * P + ord(s[i])
        
        # 查看s中是否有长度为len的重复子串
        def check(s, length):
            # 好麻烦啊,前缀问题一律都用前闭后闭的,但是切片写法就是前闭后开的
            # 最后一个串的下标为 [? , len_s-1] len_s - 1 - ? + 1 = len
            cnt = {}
            # O(n)
            for i in range(0, len(s) - length + 1):
                # O(1) -> 不用前缀优化,就是O(n)
                # [i:len-1] len-1 - i + 1 = length
                # 子串 [i:i+len-1]
                j = i + length - 1

                cur = h[j] - h[i-1] * p[j - i + 1]
                if i == 0:
                    cur = h[j]
                # print(cur, i, j)

                if cur in cnt:
                    return s[i:j+1]
                cnt[cur] = 1
            return ""


        ans = ""

        # O(n)
        for length in range(1, len(s)):
            t = check(s, length)
            if len(t) > len(ans):
                ans = t
        return ans

但是仍然通过不了,所以我们只能从题干中 "最长的子串" 下手

我们知道,如果长度为len1的子串符合要求,那么长度为len2 (len2 < len1)的子串肯定也满足要求

如果长度为len3的子串不满足要求,由于子串的连续性,一定有len4 > len3的子串也不满足要求

那么这不就是二分吗。

还是超时,我无语了啊。。。 这里顺带提一嘴,构造前缀hash数组的时候,应该定义为pre[i] 表示 前缀str[:i) 的前缀和,注意不包含右边界。

这么定义的好处在于,不需要对求 [0:j]这个case进行特判。

class Solution:
    def longestDupSubstring(self, s: str) -> str:

        n = len(s)
        h = [0] * n
        p = [0] * n
        p[0] = 1
        h[0] = ord(s[0])  
        P = 1313131  # P决定了hash冲突的概率

        # 相比一般的前缀数组,需要我们维护一个p,表示字符串每一位的贡献
        # 我们定义串的hash值为 h[i] = s[i] + s[i-1] * p + s[i-2] * p^2 + ... + s[0] * p^(i-1)
        # 这么定义的好处是,如果我们想求s[i:j]的hash值,为 s[i:j] = s[j] + s[j-1] * p + ... + s[i] * p^(j-i)
        # 注意s[i:j] 表示前闭后闭,此时有n = j - i + 1个元素,最后一个元素的指数为 n-1, 所以得出j - i
        # 这个表达式刚好为 s[j] - s[i-1] * p^(j-i+1)
        # s[j] = s[j] + ... + s[0] * p^(j-1)
        # s[i-1] = s[i-1] + ... + s[0] * p^(i-2) (此时补偿一个 j - i + 1), 得到 i-2 + (j-i+1) = j-1

        # 所以,记住结论吧 s[i:j] 前闭后闭的hash值为,pre[j] - pre[i] * p[j - i + 1]

        for i in range(1, n):
            # 注意,字符串hash是一个int
            p[i] = p[i-1] * P
            h[i] = h[i - 1] * P + ord(s[i])
        
        # 查看s中是否有长度为len的重复子串
        def check(s, length):
            # 好麻烦啊,前缀问题一律都用前闭后闭的,但是切片写法就是前闭后开的
            # 最后一个串的下标为 [? , len_s-1] len_s - 1 - ? + 1 = len
            cnt = set()
            # O(n)
            for i in range(0, len(s) - length + 1):
                # O(1) -> 不用前缀优化,就是O(n)
                # [i:len-1] len-1 - i + 1 = length
                # 子串 [i:i+len-1]
                j = i + length - 1

                cur = h[j] - h[i-1] * p[j - i + 1]
                if i == 0:
                    cur = h[j]
                # print(cur, i, j)

                if cur in cnt:
                    return s[i:j+1]
                cnt.add(cur)
            return ""


        ans = ""

        # O(n) -> O(logn)
        l = 0
        r = len(s)
        # 前闭后开
        while l < r:
            mid = (l + r) // 2
            t = check(s, mid)
            if len(t) != 0:
                # 说明长度为mid时有解,那么 <= mid的就不用看了
                l = mid + 1
            else:
                # 如果长度为mid时无解,那么 > mid的也不用看了
                r = mid 
            if len(t) > len(ans):
                ans = t
        return ans


        

最长相同子串

依旧是二分+字符串hash,考虑到对于最长子串问题,由于长度连续,所以可以二分。对于多个字符串 的最长子串,其最长一定出自 所有字符串里最短的

todo: 对于M个子串的比较,如何高效进行?

我们目前对两个子串的比较,就是先构造S1的所有长度为length的子串hash值,然后去S2看所有长度为length的子串hash是否存在于set,如果有则立刻返回。

时间复杂度

O(log(min(n,m)(m+n))O(log(min(n, m) * (m + n))

"""
    最长公共子串系列。
    给你m个字符串,求出他们最长的公共子串

    分析:
    由于子串具有连续性,所以如果长为k的子串存在,那么长度为k-1的子串也存在。
    如果长度为k的子串不存在,那么长度为k+1的子串也不存在

    延展: 如果题目中闻到求一个 连续对象的最大/最小,可以考虑是否需要二分
"""
class Solution2:



    def longest_common_substring(self, s1, s2):
        n = len(s1)
        m = len(s2)
        h1, p1 = [0] * n, [0] * n
        h2, p2 = [0] * m, [0] * m
        p1[0], p2[0] = 1, 1
        h1[0], h2[0] = ord(s1[0]), ord(s2[0])
        prime = 1313131
        for i in range(1, n):
            h1[i] = h1[i-1] * prime + ord(s1[i])
            p1[i] = p1[i-1] * prime
        for i in range(1, m):
            h2[i] = h2[i-1] * prime + ord(s2[i])
            p2[i] = p2[i-1] * prime

        # 检查s1和s2中是否存在长度为length的公共子串
        def check(s1, s2, length):
            s = set()

            # 枚举s1中长度为length的所有子串
            for i in range(len(s1) - length + 1):   # [i: i+len-1]  i+len-1 = len(s1) - 1
                j = i + length - 1
                if i == 0:
                    cur = h1[j]
                else:
                    cur = h1[j] - h1[i-1] * p1[j-i+1]
                print(cur, s1[i:j+1])
                s.add(cur)

            for i in range(len(s2) - length + 1):
                j = i + length - 1
                if i == 0:
                    cur = h2[j]
                else:
                    cur = h2[j] - h2[i-1] * p2[j-i+1]
                print(cur, s2[i:j+1])

                if cur in s:
                    # 有多个返回一个即可
                    return s2[i:j+1]
            print(s)
            return ""


        # 二分长度,最长公共子串肯定依赖于 更小的串
        length = min(n, m)

        # 前闭后开区间
        left = 0
        right = length + 1
        ans = ""
        while left < right:
            mid = (left + right) // 2
            print(mid)
            t = check(s1, s2, mid)
            print("t is", t)
            if len(t) > len(ans):
                # t和比t小的不用看了
                left = mid + 1
            else:
                # 比t大的不用看了
                right = mid
            if len(t) > len(ans):
                ans = t
        return ans

s = Solution2()

print(s.longest_common_substring("qweweedaqqmfqwe", "oooqweeda"))