“帆软杯”武汉大学程序设计竞赛 F 最短公共超串(SAM or KMP 求最短公共父串)

本文介绍了如何利用单串SAM和KMP算法解决字符串的最短公共父串问题。首先通过建立SAM(suffix automaton machine)来处理一个字符串,寻找其前缀和后缀信息,然后匹配第二个字符串,找到最长的公共后缀。如果找不到完全包含关系,再利用KMP算法检查是否存在子串关系,并给出不同情况下的解决方案。整个过程的时间复杂度为O(n)。
摘要由CSDN通过智能技术生成

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

题意:

两个字符串 的 “最短公共父串”(其含义可以类比 “最长公共子串” 进行反推)

思路:

单串 SAM

pre[i] 表示 SAM 节点 i 能否作为 s1 的前缀,suf[i] 表示 SAM 节点 i 能否作为 s1 的后缀,两者类型 均为 bool

之后,s2 匹配 SAM

匹配到 能作为 suf 的节点时,就看 当前 s2 的匹配长度是不是 i

是的话,s2 的前缀成功匹配了 s1 的一个后缀

最后遍历完后,看走到了 SAM 的哪个节点上,如果最后 这个节点的len >= s2的长度 就代表 s2s1 包含

然后 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值