- 最长相同子串
todo: dp 二分 + 字符串hash,做法和1044基本一样。
- 最长重复子串
leetcode1044 字符串hash(可以看成另类的前缀和), 后缀数组(TODO), 倍增后缀数组(n->logn)
- 模式串匹配
leetcode28 暴力, KMP
- 最长回文子串
leetcode5 dp, 中心扩散, manacher算法
- 回文子串变体
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(1)的复杂度
1. 最长回文子串
下面的改成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算法
又是学了忘忘了学系列。
超赞字符串(leetcode1542)
反正子串好几种变体,取决于题目咋定义。
暴力做法
枚举所有区间,判断区间内是不是超赞字符串。
这个超赞字符串的约束不如 回文串,基于频率统计即可。
写完之后,我们发现,
- 每个区间都需要维护词频信息,能不能弄出某个数据结构,使得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
前缀异或和
前缀的变体
刚做的时候,其实想到了"前缀数组"这个,我估计是之前做过上面那道题,"只出现一次的数字",这个题其实和这个思想差不多。
超赞字符串的特性是:基于词频求解。而且词频还要求只能存在一个奇数 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
第一次打这么富裕的仗,开了一个的数组。
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
这里有几个问题
- 如果模式串没有字符c,但是原串里匹配到了c。比如
word: ababdabacb pat: ababc
暴力匹配法
ababdabacb
ababc <=
_ <=
ab
_ <=
_ <=
ab
可以看到,尽管我们知道pat里没有d,但是为了从d走过去,以暴力的做法要走好几步。
- 如果目前串和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这个串,我们如何理解
- 定义初态为"",只有首个字符能进入下一个状态,其他字符都会在初态轮转
- 对于状态S1,只有输入为下一个字符时,才会使得状态前进,否则只能后退。
- KMP的精髓在于:后退时,不会像暴力匹配那样一次性全部后退,而是找到当前状态构成子串的前缀,并从这里后退。
ababc:
如果当前状态为 s3
a b a b c
s0 s1 s2 s3 s4
-
此时输入为C:那么前进到状态s4
dfa[s3]['c'] = s4此时有解。 -
此时输入为Z: 那么直接后退到s0,
dfa[s3]['z'] = s0,因为z不在pat中,所以遇到z只能从头匹配。(对应暴力做法中,pat的字符不在原串时的优化) -
此时输入为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;
}
}
最长重复子串
首先,子串问题是前缀问题的一种,用前缀法思考子串比较想到优化点。
其次,子串问题又是字符串问题,题目问法的多样性,需要我们把字符串处理为不同的数据结构,问题转换为在这些数据结构上进行优化的方案。
之所以比较难,因为一个问法可能就对应一个专门的数据结构,通常没见过就不会做。
甚至字符串由于其独有特性,比如模式串匹配的特性,回文串的特性,往往还有专门的算法,使得整个体系变得很庞大
- 暴力法
看到子串问题,尽管我们可以想到前缀,后缀,因为前缀j - 前缀i其实就是[i+1, j]这个子串。
那么,我们还可以定义子串为 [i, len], i表示起始下标,len表示长度,由于字串连续,知道起始下标i和长度len就能确定唯一的子串。
下面这个做法就是利用了这一点。
- 尝试以任何字符为起点
- 这个思路比较巧妙 反正是找最长的子串,肯定尝试从子串为1开始寻找,慢慢增加。这个就有点打擂台的意思了。
- 连续出现两次就算是连续子串,因此我们只需要一次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,如果有则立刻返回。
时间复杂度
"""
最长公共子串系列。
给你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"))