知识点: SAM,SA,单调栈,Hash

原题面 Luogu 来自 poj 的双倍经验

简述

给定两字符串 \(S_1, S_2\),求它们的最长公共子串长度。

\(|S_1|,|S_2|\le 2.5\times 10^5\)。

294ms,1.46GB。

分析

以下将介绍四种效率不同的写法。

图片为按最优解排序后的结果,从上到下依次为 SAM、SA、自己 YY 的 SA 和 Hash。

Hash

二分答案枚举最长公共子串长度 \(mid\)。

Check 时,将一个串所有长度为 \(mid\) 的子串的 hash 值扔到 hash 表里。

枚举另一个串所有长度为 \(mid\) 的子串,检查在 hash 表中是否存在即可。

理论复杂度 \(O(n\log n)\),但取模运算过多,所以常数巨大,实际运行效率是四种方法中最低的。

注意写单 hash 或用 set 都会被卡。

自己 YY 的 SA 做法

只有两个字符串,考虑 SA。

\(S_1\) 加个终止符,\(S_2\) 拼接到 \(S_1\) 后面,跑 SA 求出 \(\operatorname{height}\)。

问题变为 求前半段后缀 与 后半段后缀 \(\operatorname{lcp}\) 的最大值。

考虑一个很直观的暴力:

若 \(i\) 为后半段的后缀,则必有 \(i>|S_1|+1\)。

对于一个后缀 \(i>|S_1|+1\),设 \(l_i\) 为后缀排序后 最大比 \(i\) 小前半段 的后缀的 排名

即有 \(sa_{l_i}\le |S_1|,\ l+i< rk_i\),且 \(\forall l_i<k\le rk_i, sa_k>|S_1|+1\) 成立。

类似地,设 \(r_i\) 为后缀排序后 最小比 \(i\) 大前半段 的后缀的 排名

考虑 \(\operatorname{lcp}\) 的单调性。

对于一个后半段的后缀 \(i>|S_1|+1\),满足 \(\operatorname{lcp}(i,j)\) 最大的 \(j\le|S_1|\),显然为 \(l_i\) 或 \(r_i\),有

\[\max\{\operatorname{lcp}(i,j)\} = \max\{\operatorname{lcp}(l_i, i), \operatorname{lcp}(i, r_i)\}
\]

则有:

\[ans = \max_{i=|S_1|+2}^{|S_1|+|S_2|+1}\max\{\operatorname{lcp}(l_i, i), \operatorname{lcp}(i, r_i)\}
\]

先预处理,对 \(\operatorname{height}\) 建立 st 表。

\(l_i,r_i\) 可通过单调栈简单求得,计算答案时枚举后半段后缀,\(O(1)\) 查询 \(\operatorname{lcp}\) 即可。

总复杂度 \(O(n\log n)\) 级别。

一些细节:

若 \(l_i<1\) 时,该 \(l_i\) 不作出贡献,因为不存在这样的后缀。

\(r_i>|S|\) 时也没有贡献,这样的后缀已经属于后半段了。

SA

上面的算法二是自己 YY 的结果,看了题解之后,学习了以下这种,理论上最优的算法。

对于 \(sa_i,sa_{i-1}\),其 \(\operatorname{lcp} = \operatorname{height}_i\)。

考虑 \(\operatorname{lcp}\) 的单调性,有一个显然的结论:

最长公共子串为:所有满足 \(sa_{i-1}, sa_i\) 分属 前/后 半段的 \(\operatorname{height}_i\) 的最大值。

即作为答案的 \(\operatorname{lcp}(l_i,i)\) (或 \(\operatorname{lcp}(i, r_i)\)),一定有 \(l_i=rk_{i}-1\) 或 \(r_i=rk_i+1\)。

证明考虑反证法。

若 \(ans=\operatorname{lcp}(l_i, i)\),且 \(l_i < rk_i-1\)。

由 \(\operatorname{lcp}(l_i,i)=\min\limits_{j=l_i+1}^{rk_i}\operatorname{height}_j\),可知对于 \(\forall l_i<j<rk_i\),\(\operatorname{lcp}(l_i, sa_j)\ge \operatorname{lcp}(l_i,i) = ans\),取它们作为答案,答案不会变劣。

反证原结论成立,\(ans = \operatorname{lcp}(i,r_i)\) 同理。

SAM

对第一个串建 SAM,用第二个串从起始节点开始,在 SAM 上进行匹配。

若当前状态为 \(x\),如果有对应字符 \(s_i\) 的转移,直接转移即可,匹配长度 \(+1\)。

如果没有对应转移,转移到 \(\operatorname{link}(x)\),匹配长度 \(=\operatorname{len}(x)+1\) 检查有无对应转移,若没有则继续转移到 \(\operatorname{link}(\operatorname{link}(x))\),直到存在对应转移。

若始终找不到对应转移,则从根开始重新匹配。

跳 parnet 树相当于失配指针,继续利用了已匹配的部分。

匹配过程中匹配的最长长度即为答案。

复杂度 \(O(n)\),实际运行效率也非常高。

代码

SAM

//知识点: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[kMaxn];
int n, k, ans, last = 1, node_num = 1;
int ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[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 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 Work() {
scanf("%s", S + 1);
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'];
if (l > k) GetMax(ans, l);
}
}
//=============================================================
int main() {
// k = read();
scanf("%s", S + 1); n = strlen(S + 1);
for (int i = 1; i <= n; ++ i) Insert(S[i] - 'a');
Work();
printf("%d", ans);
return 0;
}

SA

//知识点:SA
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctype.h>
#define ll long long
const int kMaxn = 5e5 + 10;
//=============================================================
char S[kMaxn];
int n1, n, m, ans, cnt[kMaxn], id[kMaxn], rkid[kMaxn];
int sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1], height[kMaxn];
int MaxHeight[kMaxn][20], Log2[kMaxn];
//=============================================================
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;
}
bool cmp(int x, int y, int w) { //判断两个子串是否相等。
return oldrk[x] == oldrk[y] &&
oldrk[x + w] == oldrk[y + w];
}
void GetHeight() {
for (int i = 1, k = 0; i <= n; ++ i) {
if (rk[i] == 1) k = 0;
else {
if (k > 0) k --;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n &&
S[i + k] == S[j + k]) {
++ k;
}
}
height[rk[i]] = k;
}
}
void SuffixSort() {
m = 300;
for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
for (int p, w = 1; w < n; w <<= 1) {
p = 0;
for (int i = n; i > n - w; -- i) id[++ p] = i;
for (int i = 1; i <= n; ++ i) {
if (sa[i] > w) id[++ p] = sa[i] - w;
}
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) ++ cnt[(rkid[i] = rk[id[i]])];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i];
std ::swap(rk, oldrk);
m = 0;
for (int i = 1; i <= n; ++ i) {
m += (cmp(sa[i], sa[i - 1], w) ^ 1);
rk[sa[i]] = m;
}
}
GetHeight();
}
bool Judge(int x, int y) {
return (x <= n1 && y > n1) || (x > n1 && y < n1);
}
//=============================================================
int main() {
scanf("%s", S + 1); n1 = strlen(S + 1);
S[n1 + 1] = 'z' + 1;
scanf("%s", S + n1 + 1 + 1); n = strlen(S + 1);
SuffixSort();
for (int i = 2; i <= n; ++ i) {
if (Judge(sa[i - 1], sa[i])) GetMax(ans, height[i]);
}
printf("%d", ans);
return 0;
}

自己 YY 的 SA

//知识点:SA,单调栈
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctype.h>
#define ll long long
const int kMaxn = 5e5 + 10;
//=============================================================
char S[kMaxn];
int n1, n, m, ans, cnt[kMaxn], id[kMaxn], rkid[kMaxn];
int sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1], height[kMaxn];
int MaxHeight[kMaxn][20], Log2[kMaxn];
int top, st[kMaxn], L[kMaxn], R[kMaxn];
//=============================================================
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;
}
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] &&
oldrk[x + w] == oldrk[y + w];
}
void GetHeight() {
for (int i = 1, k = 0; i <= n; ++ i) {
if (rk[i] == 1) k = 0;
else {
if (k > 0) k --;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n &&
S[i + k] == S[j + k]) {
++ k;
}
}
height[rk[i]] = k;
}
}
int Query(int l_, int r_) {
int k = Log2[r_ - l_ + 1];
return std :: min(MaxHeight[l_][k], MaxHeight[r_ - (1 << k) + 1][k]);
}
void MakeSt() {
for (int i = 2; i <= n; ++ i) MaxHeight[i][0] = height[i];
for (int i = 2; i <= n; ++ i) {
Log2[i] = Log2[i - 1] + ((1 << Log2[i - 1] + 1) == i);
}
for (int j = 1; j < 20; ++ j) {
for (int i = 1; i + (1 << j) - 1 <= n; ++ i) {
MaxHeight[i][j] = std :: min(MaxHeight[i][j - 1],
MaxHeight[i + (1 << j - 1)][j - 1]);
}
}
}
void SuffixSort() {
m = 300;
for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
for (int p, w = 1; w < n; w <<= 1) {
p = 0;
for (int i = n; i > n - w; -- i) id[++ p] = i;
for (int i = 1; i <= n; ++ i) {
if (sa[i] > w) id[++ p] = sa[i] - w;
}
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) ++ cnt[(rkid[i] = rk[id[i]])];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i];
std ::swap(rk, oldrk);
m = 0;
for (int i = 1; i <= n; ++ i) {
m += (cmp(sa[i], sa[i - 1], w) ^ 1);
rk[sa[i]] = m;
}
}
GetHeight();
MakeSt();
}
//=============================================================
int main() {
scanf("%s", S + 1); n1 = strlen(S + 1);
S[n1 + 1] = 'z' + 1;
scanf("%s", S + n1 + 1 + 1); n = strlen(S + 1);
SuffixSort();
//注意 l,r 存的是排名,枚举时按照字典序枚举后缀。
for (int i = 1, now = 0; i <= n; ++ i) {
if (sa[i] == n1 + 1) continue ;
if (sa[i] > n1 + 1) {
L[i] = now, st[++ top] = i;
continue ;
}
for (; top; top --) R[st[top]] = i;
now = i;
}
for (; top; top --) R[st[top]] = n1 + 1;
for (int i = 1; i <= n; ++ i) {
if (sa[i] <= n1 + 1) continue ;
if (L[i]) GetMax(ans, Query(L[i] + 1, i));
if (R[i] <= n1 + 1) GetMax(ans, Query(i + 1, R[i]));
}
printf("%d", ans);
return 0;
}
/*
aabcab
adadeaf aabcab{adadeaf
abcab{adadeaf
ab{adadeaf
adadeaf
adeaf
af
bcab{adadeaf
b{adadeaf
cab{adadeaf
dadeaf
deaf
eaf
f
{adadeaf
*/

Hash

//知识点:二分答案,Hash
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e5 + 10;
const LL kMod1 = 998244353;
const LL kMod2 = 1e9 + 9;
const LL kBase = 1145141;
//=============================================================
int n1, n2, ans, e_num, head[kBase + 10], ne[kMaxn << 1];
char s1[kMaxn], s2[kMaxn];
LL pow1[kMaxn], pow2[kMaxn];
LL has11[kMaxn], has12[kMaxn], has21[kMaxn], has22[kMaxn];
LL val[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 Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Insert(LL val_) {
int pos = val_ % kBase + 1;
for (int i = head[pos]; i; i = ne[i]) {
if (val[i] == val_) return ;
}
val[++ e_num] = val_;
ne[e_num] = head[pos];
head[pos] = e_num;
}
bool Count(LL val_) {
int pos = val_ % kBase + 1;
for (int i = head[pos]; i; i = ne[i]) {
if (val[i] == val_) return true;
}
return false;
}
void Prepare() {
scanf("%s", s1 + 1);
scanf("%s", s2 + 1);
n1 = strlen(s1 + 1);
n2 = strlen(s2 + 1);
pow1[0] = pow2[0] = 1;
for (int i = 1; i < std::max(n1, n2); ++ i) {
pow1[i] = pow1[i - 1] * kBase % kMod1;
pow2[i] = pow2[i - 1] * kBase % kMod2;
}
for (int i = 1; i <= n1; ++ i) {
has11[i] = (has11[i - 1] * kBase + s1[i]) % kMod1;
has12[i] = (has12[i - 1] * kBase + s1[i]) % kMod2;
}
for (int i = 1; i <= n2; ++ i) {
has21[i] = (has21[i - 1] * kBase + s2[i]) % kMod1;
has22[i] = (has22[i - 1] * kBase + s2[i]) % kMod2;
}
}
bool Check(int lth_) {
e_num = 0;
memset(head, 0, sizeof (head));
for (int l = 1, r = lth_; r <= n1; ++ l, ++ r) {
LL now_has11 = ((has11[r] - has11[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
LL now_has12 = ((has12[r] - has12[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
Insert(now_has11 * kMod2 + now_has12);
}
for (int l = 1, r = lth_; r <= n2; ++ l, ++ r) {
LL now_has21 = ((has21[r] - has21[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
LL now_has22 = ((has22[r] - has22[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
if (Count(now_has21 * kMod2 + now_has22)) return true;
}
return false;
}
//=============================================================
int main() {
// freopen("A.txt", "r", stdin);
Prepare();
for (int l = 1, r = std::min(n1, n2); l <= r; ) {
int mid = (l + r) >> 1;
if (Check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
printf("%d\n", ans);
return 0;
}
/*
opawmfawklmiosjcas1145141919810asopdfjawmfwaiofhauifhnawf
opawmdawlmioaszhcsan1145141919810bopdjawmdaw
*/

「双串最长公共子串」SP1811 LCS - Longest Common Substring的更多相关文章

  1. URAL 1517 Freedom of Choice (后缀数组 输出两个串最长公共子串)

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/whyorwhnt/article/details/34075603 题意:给出两个串的长度(一样长) ...

  2. [Python]最长公共子序列 VS 最长公共子串[动态规划]

    前言 由于原微软开源的基于古老的perl语言的Rouge依赖环境实在难以搭建,遂跟着Rouge论文的描述自行实现. Rouge存在N.L.S.W.SU等几大子评估指标.在复现Rouge-L的函数时,便 ...

  3. SPOJ 1811 Longest Common Substring (后缀自动机第一题,求两个串的最长公共子串)

    题目大意: 给出两个长度小于等于25W的字符串,求它们的最长公共子串. 题目链接:http://www.spoj.com/problems/LCS/ 算法讨论: 二分+哈希, 后缀数组, 后缀自动机. ...

  4. POJ 3294 n个串中至少一半的串共享的最长公共子串

    Life Forms Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 12484   Accepted: 3502 Descr ...

  5. SPOJ 1811 Longest Common Substring(求两个串的最长公共子串 || 或者n个串)

    http://www.spoj.com/problems/LCS/ 题目:求两个串的最长公共子串 参考:https://www.cnblogs.com/autoint/p/10345276.html: ...

  6. BZOJ 2946 POI2000 公共串 后缀自动机(多串最长公共子串)

    题意概述:给出N个字符串,每个串的长度<=2000(雾...可能是当年的年代太久远机子太差了),问这N个字符串的最长公共子串长度为多少.(N<=5) 抛开数据结构,先想想朴素做法. 设计一 ...

  7. 【poj1226-出现或反转后出现在每个串的最长公共子串】后缀数组

    题意:求n个串的最长公共子串,子串出现在一个串中可以是它的反转串出现.总长<=10^4. 题解: 对于每个串,把反转串也连进去.二分长度,分组,判断每个组. #include<cstdio ...

  8. SPOJ LCS2 多个串的最长公共子串

    这里串最多有10个,找所有串的最长公共子串 这里后缀自动机做,以第一个串建立后缀自动机,后面的串一个个去匹配,每次得到当前串在可到达状态上所能得到的最长后缀长度 拿所有串匹配后得到的结果进行计算 #i ...

  9. 多个串的最长公共子串 SPOJ - LCS2 后缀自动机

    题意: 求多个串的最长公共子串 这里用的是O(n)的后缀自动机写法 我后缀数组的专题有nlog(n)写法的 题解: 对于其中的一个串建立后缀自动机 然后对于后缀自动机上面的每一个节点求出每一个节点最长 ...

随机推荐

  1. 《手把手教你》系列技巧篇(四十八)-java+ selenium自动化测试-判断元素是否可操作(详解教程)

    1.简介 webdriver有三种判断元素状态的方法,分别是isEnabled,isSelected 和 isDisplayed,其中isSelected在前面的内容中已经简单的介绍了,isSelec ...

  2. adhere, adjust, adjacent

    adhere to stick,不是to here. 在古英语里,stick是twig(细树枝).fasten(想必是用twig来固定).后引申为粘住.stick还有stab, pierce的意思,想 ...

  3. 23. 关于Ubuntu中Could not get lock /var/lib/dpkg/lock解决方案

    原文:https://blog.csdn.net/u011596455/article/details/60322568 版权声明:本文为博主原创文章,转载请附上博文链接! 在Ubuntu中,有时候运 ...

  4. 商业爬虫学习笔记day3

    一. 付费代理发送请求的两种方式 第一种方式: (1)代理ip,形式如下: money_proxy = {"http":"username:pwd@192.168.12. ...

  5. 【C#】【MySQL】C#连接MySQL数据库(一)代码

    C#连接MySQL数据库 准备工作 1.环境安装 安装MySQL For Visual Studio<<点击进入官网下载 第一个要下载安装,第二个下载后将MySQL.data添加到Visu ...

  6. 手写IOC实践

    一.IOC 1.什么是IOC? 控制反转(英语:Inversion of Control,缩写为IoC),是[面向对象编程]中的一种设计原则,可以用来减低计算机代码之间的[耦合度]其中最常见的方式叫做 ...

  7. Docker从入门到精通(二)——安装Docker

    通过上面文章,我们大概知道了什么是Docker,但那都是文字功夫,具体想要理解,还得实操,于是这篇文章带着大家来手动安装Docker. 1.官方教程 https://docs.docker.com/e ...

  8. scrapy爬取招聘网站,items转换成dict遇到的问题

    pipelines代码 1 import json 2 3 class TencentJsonPipeline(object): 4 def __init__(self): 5 self.file = ...

  9. CF893B Beautiful Divisors 题解

    Content 给定一个数 \(n\),求出 \(n\) 最大的可以表示成 \((2^k-1)\cdot2^{k-1}\) 形式的因数 \(x\). 数据范围:\(1\leqslant n\leqsl ...

  10. WinFrm中多线程操作窗体属性

    首先声明一个委托. delegate void SetTextCallback(string text); 然后再写一个事件. private void SetInfo(string text) { ...