「多串最长公共子串」SP1812 LCS2 - Longest Common Substring II
知识点: SAM
Luogu
题意简述:
给定不超过 \(10\) 个字符串 \(S_1, S_2, \cdots\),求它们最长公共子串的长度。
\(|S_i|\le 10^5\)。
分析题意
多串最长公共子串问题,考虑 SAM。
如果只有两个串:SP1811 LCS - Longest Common Substring
对第一个串建 SAM,用第二个串从起始节点开始,在 SAM 上进行匹配。
若当前状态为 \(x\),如果有对应字符 \(s_i\) 的转移,直接转移即可,匹配长度 \(+1\)。
如果没有对应转移,转移到 \(\operatorname{link}(x)\),匹配长度 \(=\operatorname{len}(x)+1\) 检查有无对应转移,若没有则继续转移到 \(\operatorname{link}(\operatorname{link}(x))\),直到存在对应转移。
若始终找不到对应转移,则从根开始重新匹配。
跳 parnet 树相当于失配指针,继续利用了已匹配的部分。
匹配过程中匹配的最长长度即为答案。
考虑多串,对第一个串 \(S_1\) 建 SAM,用其他串在 SAM 上匹配,设当前匹配到串 \(S_i\)。
对于状态 \(u\),维护转移到它时最大的匹配长度 \(mx_u\),即以该状态作为后缀时的公共子串的最长长度。
在匹配过程中进行维护即可。
考虑一个状态 parent 树上的所有祖先,若该状态可被匹配到,则祖先也可被匹配到。
祖先的 \(mx\) 应为 其子树中 \(mx\) 的最大值。
\(S_i\) 匹配完成后按拓扑序对祖先的信息进行更新。
对于一个状态 \(u\),将 \(S_2\cdots S_n\) 匹配时的 \(mx_u\) 取 \(\min\),得到在所有字符串中 转移到 \(u\) 最长的匹配长度,即以 \(u\) 为后缀时 \(S_2\cdots S_n\) 公共子串的最长长度,设为 \(mi_u\)。
所有的 \(mi_u\) 取最大值,即为答案。
小细节
最长公共子串不会超过最短串的长度,应对最短的串建 SAM,以保证复杂度。
注意每次匹配完一个字串时,都将 \(mx\) 清空。
更新祖先信息时 应对祖先的 \(\operatorname{len}\) 取 \(\min\)。
代码实现
//知识点:SAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 3e5 + 10;
const int kMaxm = 26;
//=============================================================
char S[11][kMaxn];
int n, T, last = 1, node_num = 1, ans;
int cnt[kMaxn], id[kMaxn];
int ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
int minn[kMaxn << 1], maxx[kMaxn << 1];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void Insert(int c_) {
int p = last, now = last = ++ node_num;
len[now] = len[p] + 1;
for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
if (! p) {link[now] = 1; return ;}
int q = ch[p][c_];
if (len[q] == len[p] + 1) {link[now] = q; return ;}
int newq = ++ node_num;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
link[newq] = link[q], len[newq] = len[p] + 1;
link[q] = link[now] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
}
void TopSort() {
for (int i = 1; i <= node_num; ++ i) cnt[len[i]] ++;
for (int i = 1; i <= node_num; ++ i) cnt[i] += cnt[i - 1];
for (int i = 1; i <= node_num; ++ i) id[cnt[len[i]] --] = i;
}
void Work(char *S_) {
int n = strlen(S_ + 1), now = 1, l = 0;
for (int i = 1; i <= n; ++ i) {
while (now && ! ch[now][S_[i] - 'a']) {
now = link[now];
l = len[now];
}
if (! now) {
now = 1;
l = 0;
continue ;
}
++ l;
now = ch[now][S_[i] - 'a'];
GetMax(maxx[now], l);
}
for (int i = node_num; i; -- i) {
int u = id[i], fa = link[u];
GetMax(maxx[fa], std :: min(maxx[u], len[fa]));
GetMin(minn[u], maxx[u]);
maxx[u] = 0; //注意清空,准备下一次匹配。
}
}
//=============================================================
int main() {
int min_len = 1e9 + 2077, min_pos;
while (~ scanf("%s", S[++ T] + 1)) {
int now_len = strlen(S[T] + 1);
if (now_len < min_len) {
min_len = now_len;
min_pos = T;
}
}
std :: swap(S[1], S[min_pos]);
for (int i = 1; i <= min_len; ++ i) Insert(S[1][i] - 'a');
TopSort();
memset(minn, 63, sizeof (minn));
for (int i = 2; i < T; ++ i) Work(S[i]);
for (int i = 1; i <= node_num; ++ i) GetMax(ans, minn[i]);
printf("%d", ans);
return 0;
}