知识点: 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. 基于MQTT协议实现远程控制的"智能"车

    智能,但不完全智能 虽然我不觉得这玩意儿有啥智能的,但都这么叫就跟着叫喽. 时隔好几天才写的 其实在写这篇博文的时候我已经在做升级了,并且已经到了中后期阶段了. 主要是业余时间做着玩,看时间了. 规格 ...

  2. day05 django框架之路由层

    day05 django框架之路由层 今日内容概要 简易版django请求声明周期流程图(重要) 路由匹配 无名有名分组 反向解析 无名有名解析 路由分发 名称空间 伪静态 虚拟环境 简易版djang ...

  3. JS去除对象或数组中的空值('',null,undefined,[],{})

    javascript去掉对象或数组中的'',null,undefined,[],{}.思路就是创建一个新的空对象,然后对传入的对象进行遍历,只把符合条件的属性返回,保留有效值,然后就相当于把空值去掉了 ...

  4. 链式栈——Java实现

    1 package struct; 2 3 //接口 4 interface ILinkStack{ 5 //栈中元素个数(栈大小) 6 int size(); 7 //取栈顶元素 8 Object ...

  5. [学习总结]5、Android的ViewGroup中事件的传递机制(二)

    下面是第一篇的连接 Android的ViewGroup中事件的传递机制(一) 关于onInterceptTouchEvent和onTouchEvent的详细解释. 1 public class Mai ...

  6. Zookeeper的选举算法和脑裂问题

    ZK介绍 ZK = zookeeper ZK是微服务解决方案中拥有服务注册发现最为核心的环境,是微服务的基石.作为服务注册发现模块,并不是只有ZK一种产品,目前得到行业认可的还有:Eureka.Con ...

  7. CentOs 7 yum 安装Nginx

    打开官网下载文档:http://nginx.org/en/download.html 2进入操作系统 centOs 7,建立文件夹 nginx ,进入nginx ,拷贝 上图1编辑命令:/etc/yu ...

  8. Socket通信和多线程的总结

    1.ServerSocket进行多线程接收 package com.yh.chat; import java.io.IOException; import java.net.ServerSocket; ...

  9. 1.Thmeleaf模板引擎

    1.Thmeleaf的基本语法 大部分的Thmeleaf表达式都直接被设置到HTML节点中,作为HTML节点的一个属性存在,这样同一个模板文件既可以使用浏览器直接打开,也可以放到服务器中用来显示数据, ...

  10. python自带性能强悍的标准库 itertools

    可迭代对象就像密闭容器里的水,有货倒不出 itertools是python内置的标准模块,提供了很多简洁又高效的专用功能,使用得当能够极大的简化代码行数,同时所有方法都是实现了生成器函数,这就意味着极 ...