题意:
求 两个字符串 的 “最短公共父串”(其含义可以类比 “最长公共子串” 进行反推)
思路:
单串 SAM
pre[i]
表示 SAM
节点 i
能否作为 s1
的前缀,suf[i]
表示 SAM
节点 i
能否作为 s1
的后缀,两者类型 均为 bool
型 。
之后,s2
匹配 SAM
,
匹配到 能作为 suf
的节点时,就看 当前 s2
的匹配长度是不是 i
。
是的话,s2
的前缀成功匹配了 s1
的一个后缀
最后遍历完后,看走到了 SAM
的哪个节点上,如果最后 这个节点的len >= s2
的长度 就代表 s2
被 s1
包含
然后 在 fa
树上跳看有没有能作为 pre
的节点
有且 节点长度 <=
最后 s2
匹配的长度 就代表 s2
的后缀匹配到了 s1
的一个前缀
时间复杂度:
O ( n ) O(n) O(n)
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
//#define map unordered_map
//int128 ORZ
/*
__int128 read() {
__int128 x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-')f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
void print(__int128 x) {
if (x < 0) {
putchar('-');
x = -x;
}
if (x > 9) print(x / 10);
putchar(x % 10 + '0');
}
*/
const int N = 2e5 + 10, M = N << 1;
int tot = 1, np = 1;
int len[M], fa[M];
int ch[M][26];
string s1, s2;
bool pre[M], suf[M]; //pre[i] 表示 节点 i 能否作为 s1 的前缀 suf[i] 表示 节点 i 能否作为 s1 的后缀(注意每个节点代表的都是一类串的集合)
int topo[M], hah[M];
int n, m;
void extend(int c, int idx) {
int p = np;
np = ++tot;
pre[np] = true; //以从前往后遍历的字符显然都可以作为前缀
if (idx == n) suf[np] = true; //最后一个字符显然可以作为后缀
len[np] = len[p] + 1;
while (p && !ch[p][c]) {
ch[p][c] = np;
p = fa[p];
}
if (!p) {
fa[np] = 1;
}
else {
int q = ch[p][c];
if (len[q] == len[p] + 1) fa[np] = q;
else {
int nq = ++tot;
len[nq] = len[p] + 1;
fa[nq] = fa[q], fa[q] = fa[np] = nq;
while (p && ch[p][c] == q) {
ch[p][c] = nq;
p = fa[p];
}
memcpy(ch[nq], ch[q], sizeof ch[q]);
}
}
}
signed main()
{
cin >> s1 >> s2;
n = s1.size(), m = s2.size();
s1 = " " + s1, s2 = " " + s2;
if (n < m) swap(s1, s2), swap(n, m);
for (int i = 1; i <= n; ++i) extend(s1[i] - 'a', i);
//计数排序求后缀链接树的拓扑序
for (int i = 1; i <= tot; ++i) ++hah[len[i]];
for (int i = 1; i <= tot; ++i) hah[i] += hah[i - 1];
for (int i = 1; i <= tot; ++i)
{
topo[hah[len[i]]] = i;
--hah[len[i]];
}
//后缀链接树自下而上更新每个节点 能否作为s1后缀 的情况
for (int i = tot; i >= 2; --i) suf[fa[topo[i]]] |= suf[topo[i]];
//拿s2前缀匹配s1的后缀
int p = 1, curlen = 0; //curlen 表示当前 s2 的匹配长度
int idx1 = 0, idx2 = 0;
for (int i = 1; i <= m; ++i)
{
int c = s2[i] - 'a';
while (p && !ch[p][c]) p = fa[p], curlen = len[p];
if (ch[p][c]) p = ch[p][c], ++curlen;
else p = 1, curlen = 0;
if (suf[p] && curlen == i) idx2 = max(idx2, curlen);
}
//拿s2后缀匹配s1的前缀
while (p > 1)
{
if (pre[p] && len[p] <= curlen) idx1 = max(idx1, len[p]);
p = fa[p];
}
string ans;
if (curlen == m) ans = s1.substr(1);
else if (curlen < m)
{
if (idx1 > idx2) ans = s2.substr(1, m - idx1) + s1.substr(1); //s2后缀 与 s1前缀 有匹配的部分
else ans = s1.substr(1, n - idx2) + s2.substr(1); //s1后缀 与 s2前缀 有匹配的部分
}
cout << ans << '\n';
return 0;
}
法二:KMP
若 s1 为 s2 的子串,那么 s2 就是答案
若 s2 为 s1 的子串,那么 s1 就是答案。
快速判断一个字符串中是否存在等于另一个字符串的子串,可以用 kmp 算法。
若不存在以上两种情况,那么答案应该是 s1 在前,s2 在后,去掉中间重叠部分,或者 s2 在前,s1 在后,去掉中间重叠部分的形式。
对于 s1 在前 s2 在后的情况,实际上我们 需要找出最长的同时为 s1
后缀和 s2
前缀的公共子串长度 len1
,那么 答案就是 |s1|+ |s2| - len1
。
将 s2 和 s1 按顺序拼接起来,得到一个新的字符串,记为 ss,对 ss 求一次 ne 数组,那么 ne[|s1|+ |s2|]
就是 ss 最大的相等前后缀长度,同时也是要求的 len1
相对应的,可以得到 s2 在前 s1 在后的情况,求得 len1。我们要令答案尽量短,就希望重叠部分尽量长,因此取重叠部分较长的那种情况即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
//#define map unordered_map
//int128 ORZ
/*
__int128 read() {
__int128 x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-')f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
void print(__int128 x) {
if (x < 0) {
putchar('-');
x = -x;
}
if (x > 9) print(x / 10);
putchar(x % 10 + '0');
}
*/
const int N = 2e5 + 10, M = N << 1;
int n, m, ne[M];
string s1, s2, ss;
int getnx(string s, int n)
{
for (int i = 2, j = 0; i <= n; ++i)
{
while (j && s[i] != s[j + 1]) j = ne[j];
if (s[i] == s[j + 1]) ++j;
ne[i] = j;
}
return ne[n];
}
bool kmp(string s1, int n, string s2, int m)
{
for (int i = 1, j = 0; i <= n; ++i)
{
while (j && s1[i] != s2[j + 1]) j = ne[j];
if (s1[i] == s2[j + 1]) ++j;
if (j == m) return true;
}
return false;
}
inline void solve()
{
cin >> s1 >> s2;
n = s1.size(), m = s2.size();
s1 = " " + s1, s2 = " " + s2;
// s2为s1的子串
getnx(s2, m);
if (kmp(s1, n, s2, m)) {
cout << s1.substr(1) << '\n';
return;
}
// s1为s2的子串
getnx(s1, n);
if (kmp(s2, m, s1, n)) {
cout << s2.substr(1) << '\n';
return;
}
// s1在前,s2在后,去掉重叠部分
ss = s2 + s1.substr(1);
int len1 = getnx(ss, n + m); // 同时为s1后缀和s2前缀的最大子串长度
// s2在前,s1在后,去掉重叠部分
ss = s1 + s2.substr(1);
int len2 = getnx(ss, n + m); // 同时为s2后缀和s1前缀的最大子串长度
string ans;
if (len1 >= len2) ans = s1.substr(1) + s2.substr(len1 + 1);
else ans = s2.substr(1) + s1.substr(len2 + 1);
cout << ans << '\n';
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(nullptr), cout.tie(nullptr);
int _ = 1; //cin >> _;
while (_--)
{
solve();
}
return 0;
}