字符串入门指南
前言
此文章带领入门基础字符串,内容从 KMP 到 SA,其中包含算法文章推荐/算法讲解,经典题目的讲解。
带 !号的题是基础例题,带 * 号的是推荐首先完成的题(有一定启发性的)。
本题单以每种字符串算法为大结构。
manacher
code
#include<bits/stdc++.h>
using namespace std;
const int N=2.2*1e7;
char s[N];
int n,p[N];
void build()
{
char c[N];
scanf("%s",c+1);
int len=strlen(c+1);
s[++n]='@';s[++n]='#';
for(int i=1;i<=len;i++) s[++n]=c[i],s[++n]='#';
s[++n]='!';
}
int main()
{
build();
int mid=0,mr=0,ans=0;
for(int i=2;i<n;i++)
{
if(i<=mr) p[i]=min(p[mid*2-i],mr-i+1);
else p[i]=1;
while(s[i+p[i]]==s[i-p[i]]) ++p[i];
if(p[i]+i>mr) mr=p[i]+i-1,mid=i;
ans=max(ans,p[i]);
}
printf("%d\n",ans-1);
return 0;
}
例题
P3501 [POI2010] ANT-Antisymmetry
就是把两字符相等换成两字符相反。
*P4555 [国家集训队] 最长双回文串
枚举两个回文串的分隔点,在 manacher 中记录两个数组:
但 manacher 求出的是最长回文串,而中间小的字串没有记录到,所以还要在递归求解一下:
P6216 回文匹配
先跑 KMP,记录每个匹配成功的左下标,再跑 manacher(只用求奇数长度,不用在中间插字符)。
如图:
我们在红串的开头做了标记(即为 sum
),当要记录
我们不可能把所有字符串都算一遍,所以枚举回文串的对称中心,得到此中心的最长回文串,记录
可以发现就是
这里再做一次前缀和即可。
KMP/ex-KMP
好的讲解:KMP算法详解
code:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char a[N],b[N];
int p[N];
int main()
{
scanf("%s",a+1);scanf("%s",b+1);
int la=strlen(a+1),lb=strlen(b+1);
// 预处理 p[] /nxt[] 数组
p[1]=0;
int j=0;
for(int i=2;i<=lb;i++)
{
while(j&&b[j+1]!=b[i]) j=p[j];
if(b[j+1]==b[i]) j++;
p[i]=j;
}
// 匹配
j=0;
for(int i=1;i<=la;i++)
{
while(j&&b[j+1]!=a[i]) j=p[j];
if(b[j+1]==a[i]) j++;
if(j==lb)
{
printf("%d\n",i-lb+1);
j=p[j];
}
}
for(int i=1;i<=lb;i++) printf("%d ",p[i]);
return 0;
}
好记忆的 exKMP 模板。
#include<bits/stdc++.h>
using namespace std;
const int N=2e7+10;
char a[N],b[N];
int la,lb;
int z[N],p[N];
void get_Z()
{
z[1]=lb;
for(int i=2,l=0,r=0;i<=lb;i++)
{
if(i<=r) z[i]=min(z[i-l+1],r-i+1);
while(i+z[i]<=lb&&b[z[i]+i]==b[z[i]+1]) ++z[i];
if(i+z[i]-1>r) r=i+z[i]-1,l=i;
}
// for(int i=1;i<=lb;i++) cout<<z[i]<<" "; cout<<endl;
}
int main()
{
scanf("%s",a+1);scanf("%s",b+1);
la=strlen(a+1),lb=strlen(b+1);
get_Z();
for(int i=1,l=0,r=0;i<=la;i++)
{
if(i<=r) p[i]=min(z[i-l+1],r-i+1);
while(i+p[i]<=la&&b[p[i]+1]==a[p[i]+i]) p[i]++;
if(i+p[i]-1>r) r=i+p[i]-1,l=i;
}
// for(int i=1;i<=la;i++) cout<<p[i]<<" "; cout<<endl;
long long ans1=0,ans2=0;
for(int i=1;i<=lb;i++) ans1^=1ll*i*(z[i]+1);
for(int i=1;i<=la;i++) ans2^=1ll*i*(p[i]+1);
printf("%lld\n%lld",ans1,ans2);
return 0;
}
例题
!失配树
只要充分了解了
上题是单个跳,还可以路径压缩,那这题就是两个一起跳,直到跳到同一深度,这里就和求 LCA 一样了。(注:LCA 不能是给定的两个结点)。
code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m;
char s[N];
int f[N][22], dep[N], d[N];
int lca(int u, int v)
{
if (dep[u] < dep[v])
swap(u, v);
for (int i = 20; i >= 0; i--)
if (dep[f[u][i]] >= dep[v])
u = f[u][i];
// if (u == v) 这里求的 LCA 不能是给的两个结点
// return u;
for (int i = 20; i >= 0; i--)
{
if (f[u][i] != f[v][i])
u = f[u][i], v = f[v][i];
}
return f[u][0];
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
f[1][0] = 0, dep[1] = 1;
for (int i = 2, j = 0; i <= n; i++)
{
while (j && s[j + 1] != s[i])
j = f[j][0];
if (s[j + 1] == s[i])
j++;
f[i][0] = j;
dep[i] = dep[j] + 1;
}
// for (int i = 1; i <= n; i++)
// cout << f[i][0] << " ";
// cout << endl;
for (int i = 1; i <= 20; i++)
for (int j = 1; j <= n; j++)
f[j][i] = f[f[j][i - 1]][i - 1];
scanf("%d", &m);
while (m--)
{
int u, v;
scanf("%d%d", &u, &v);
printf("%d\n", lca(u, v));
}
return 0;
}
P2375 [NOI2014] 动物园
有了失配树的想法,此题很容易想到倍增优化,先跳到比
记得倍增时把小的那一位放前面,寻址快,优化常数。
但此题正解是
我们知道每次每次都重新跳容易卡成
当然,
AC 自动机
算法及模板
注:此算法是建立在会 Trie 的前提下。
好的学习博客:yyb的博客,其实 oi wiki 的图也做得不错。
补充:
-
fail 指针指向所有模式串的前缀中匹配当前状态的最长后缀(有点像 KMP)。
-
这里把 Trie 树改成了 Trie 图, fail 指针跳转的路径做了压缩(就像并查集的路径压缩),使得本来需要跳很多次 fail 指针变成跳一次。
第一题 code:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
struct node
{
int end, fail, son[30];
} tr[N];
int n, cnt = 1;
void build_tree()
{
char s[N];
scanf("%s", s + 1);
int len = strlen(s + 1);
int u = 1;
for (int i = 1; i <= len; i++)
{
int v = s[i] - 'a';
if (!tr[u].son[v])
tr[u].son[v] = ++cnt;
u = tr[u].son[v];
// cout << u << " " << s[i] << endl;
}
tr[u].end++;
}
void get_fail()
{
for (int i = 0; i < 26; i++)
tr[0].son[i] = 1;
queue<int> q;
q.push(1);
tr[1].fail = 0;
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
int v = tr[u].son[i];
if (!v)
tr[u].son[i] = tr[tr[u].fail].son[i];
else
{
tr[v].fail = tr[tr[u].fail].son[i];
q.push(v);
}
}
}
}
void ac_ask()
{
char s[N];
scanf("%s", s + 1);
int len = strlen(s + 1);
int u = 1, ans = 0;
for (int i = 1; i <= len; i++)
{
int v = s[i] - 'a';
int k = tr[u].son[v];
while (k > 1 && tr[k].end != -1)
{
ans += tr[k].end;
tr[k].end = -1;
k = tr[k].fail;
}
u = tr[u].son[v];
}
printf("%d\n", ans);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
build_tree();
get_fail();
ac_ask();
return 0;
}
第三题拓扑优化:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 6;
int n, cnt, sum[N], in[N], mp[N];
struct node
{
int son[27], end, fail, ans;
void init()
{
memset(son, 0, sizeof son);
ans = end = fail = 0;
}
} tr[N];
char s[N];
void build_tree(int id)
{
scanf("%s", s + 1);
int len = strlen(s + 1), u = 1;
for (int i = 1; i <= len; i++)
{
int v = s[i] - 'a';
if (!tr[u].son[v])
tr[u].son[v] = ++cnt;
u = tr[u].son[v];
}
if (!tr[u].end)
tr[u].end = id;
mp[id] = tr[u].end;
}
void get_fail()
{
for (int i = 0; i < 26; i++)
tr[0].son[i] = 1;
queue<int> q;
q.push(1);
tr[1].fail = 0;
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
int v = tr[u].son[i];
if (!v)
tr[u].son[i] = tr[tr[u].fail].son[i];
else
{
tr[v].fail = tr[tr[u].fail].son[i];
in[tr[v].fail]++;
q.push(v);
}
}
}
}
void ac_ask()
{
char c[N];
scanf("%s", c + 1);
int len = strlen(c + 1), u = 1;
for (int i = 1; i <= len; i++)
{
int v = c[i] - 'a';
u = tr[u].son[v];
tr[u].ans++;
}
}
void topu()
{
queue<int> q;
for (int i = 1; i <= cnt; i++)
if (!in[i])
q.push(i);
while (!q.empty())
{
int u = q.front();
q.pop();
int v = tr[u].fail;
sum[tr[u].end] += tr[u].ans;
tr[v].ans += tr[u].ans;
in[v]--;
if (!in[v])
q.push(v);
}
}
int main()
{
cnt = 1;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
build_tree(i);
get_fail();
ac_ask();
topu();
for (int i = 1; i <= n; i++)
printf("%d\n", sum[mp[i]]);
return 0;
}
例题
P3966 [TJOI2013] 单词
其实就是模板。
模板中记录次数是文本串的字符的
void build_tree(int id)
{
scanf("%s", s + 1);
int len = strlen(s + 1), u = 1;
for (int i = 1; i <= len; i++)
{
int v = s[i] - 'a';
c[++tot] = s[i];
if (!tr[u].son[v])
tr[u].son[v] = ++cnt;
u = tr[u].son[v];
tr[u].ans++; //添加的地方。
}
if (!tr[u].end)
tr[u].end = id;
mp[id] = tr[u].end;
}
P3121 [USACO15FEB] Censoring G
有些时候只是用了建好的新 Trie 图。
这个新的 Trie 图每个结点都连了 26 条边,所以在最后字符串匹配时能保证
而 fail 既完成了新的 Trie 图的建立,也可以在匹配阶段完成每个模式串出现的出现(如同模板)。
在 get_fail 时也用了前面求过的 fail,就如同 KMP 的 nxt 的求法。
此题只用建好 Trie 图,然后用栈维护,一旦找到就 pop,最后栈里剩下就是要求的字符串。
*P2444 [POI2000] 病毒
先建好 Trie 图,在图上做 dfs,不走有病毒标志的结点。
因为找的串是无限循环的字符串,就是要找到循环节,所以找到一个环即为找到循环节,就返回 ture。
注意:如果一个结点的 fail 有标志,那它也应打上标记,因为根据 fail 的定义,fail 结点的前缀为当前结点的最长公共后缀,既然 fail 结点是个完整的字符串,那说明此节点所在字符串包含 fail 结点的字符串,显然是不符条件,所以也打上标记。
上面两道题都用的是 Trie 图。
*P2414 [NOI2011] 阿狸的打字机
很好的一道题。
- 40分做法:
-
最暴力的方法:跑
次 KMP。 -
建一个 AC 自动机,每次询问时一直跳 fail,找
串中出现了多少次 串。
- 70分做法
上面的40分做法一直在跳 fail,所以想到建棵失配树。
树一般都是从上往下的,所以我们反向,就是求
这里可以用树状数组,线段树等数据结构,但树状数组常数小又好写,此题肯定用它了。
先 dfs 一下求一下 dfn,可以知道一个子树的 dfn 一定是连续的,当出现
如果是
时间复杂度:
- 100分做法
上面的方法还是跑了许多重复的子树,考虑在线加点与删点。
当枚举到一个结点时
此题的细节:
-
第二次 dfs 时用旧的的 Trie 树。
-
注意建 Trie 树的时间复杂度。
以上的题目用到了 AC 自动机的两种用途:使用新建的 Trie 图与用 fail 数组建立失配树。
后缀数组(SA)
算法讲解
注 :此节完全引用机房大佬 Comentropy 的文章(个人觉得写的好),由于他未发表,所以把文章引用于此。
基数排序就是先按第一关键字排序,相同时比较第二关键字,再相同时比较第三关键字,依次类推。
后缀数组分为两部分,第一部分是后缀数组本身,求解后缀数组来表示后缀的字典序排名,第二部分将会使用后缀数组将他们和子串关联起来。
这一部分参照了 OI Wiki 的相关内容,特此注明。
再次提醒默认下标从 C++
中的下标记法
后缀数组
后缀数组 (Suffix Array)
主要关系到两个数组,
表示后缀排序后第 小的后缀编号,这就是所谓后缀数组。 表示后缀 的排名。
满足一个重要性质:
我们很显然有
很显然,排序具有可合并性,所以我们可以进行倍增:具体地,以上一轮每个后缀的排名为第一关键字,以长度
算法除了基数排序的部分,大致实现基本都很简单。以下为大致实现:
- 预处理出
时的排名(注意,请对倍增有清晰的了解,我们本质上是在从 转移到 的状态,并不是在处理 )。然后开始倍增字串长度 ,即所有后缀 目前只处理了 。 - 先对于第二关键字进行排序,处理出第二关键字有序的对应后缀开头
。 - 然后对第一关键字进行排序,若两个关键字相同,则暂时共用一个排名,否则比较。这一步,我们其实已经把上一轮的第一关键字排好了,直接按顺序比较即可。
- 如此处理直到
.
结合代码具体阐释(模板题 P3809):
int n=strlen(s+1),m=122;
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;
这一部分是基数排序的基本操作,设值域为 z
的 ASCII
码。然后设置初始
for(int k=1;k<=n;k<<=1)
倍增正常操作,目的是从
int num=0;
for(int i=n-k+1;i<=n;i++)
id[++num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>k)
id[++num]=sa[i]-k;
for(int i=1;i<=m;i++) cnt[i]=0; // 按值域清空
for(int i=1;i<=n;i++) cnt[rk[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i],id[i]=0;
类似地统计第一关键字(即排名)并前缀和,而最后一行与之前不太一样。倒序地,我们获取了第二关键字排名为
我们更新完了
std::swap(rk,id);
num=1,rk[sa[1]]=1;
for(int i=2;i<=n;i++)
if(id[sa[i]]==id[sa[i-1]]&&id[sa[i]+k]==id[sa[i-1]+k])
rk[sa[i]]=num;
else
rk[sa[i]]=++num;
if(num==n)
break ;
m=num;
上一步已经将
先赋初值,
如果两个关键字都相同则排名相同,否则排名不同,而注意到其实此时排名已经确定,即
于是完成了
code
#include<cstdio>
#include<cstring>
#include<algorithm>
const int N=1e6+500;
int sa[N],rk[N],id[N],cnt[N];
char s[N];
void SA(){
int n=strlen(s+1),m=122;
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 k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++)
id[++num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>k)
id[++num]=sa[i]-k;
for(int i=1;i<=m;i++) cnt[i]=0;
for(int i=1;i<=n;i++) cnt[rk[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i],id[i]=0;
std::swap(rk,id);
num=1,rk[sa[1]]=1;
for(int i=2;i<=n;i++)
if(id[sa[i]]==id[sa[i-1]]&&id[sa[i]+k]==id[sa[i-1]+k])
rk[sa[i]]=num;
else
rk[sa[i]]=++num;
if(num==n)
break ;
m=num;
}
for(int i=1;i<=n;i++)
printf("%d ",sa[i]);
return ;
}
int main(){
scanf("%s",s+1);
SA();
return 0;
}
事实上,还有 DC3
,SA-IS
的线性算法可以完成任务,这里不再介绍。
数组
两个字符串
记后缀 LCP(Longest Common Preffix)
,长度 为
(特别注意在做题时个别题解的表述出现了问题。)
定义
去
- 由于
非负,所以当 时,上式成立。以下讨论 的情况。
根据定义:
由上式:设后缀 LCP
,为字符串
则又可设
于是可以向后转移:
又由于后缀
显然,
所以:
看张图手动跑一下更好理解
如图中的黑色后缀(saff(
与图中的红色后缀(saff(
由此可以实现:
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1)
continue ;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k])
k++;
h[rk[i]]=k;
}
特别注意
数组的应用
- 两后缀公共前缀
后缀排名为
这是一个较为显然的性质,称为 LCP Theorem,感性理解即可(严格证明较为麻烦)。这转化成一个 RMQ
问题。
- 本质不同子串个数
因为 子串就是后缀的前缀,所以我们按 后缀排名 枚举每个后缀,在前缀数中减去重复前缀。设当前枚举到了排名
(如果已经理解,无需理会这句话)特别注意,我们无需管与后面的是否相重,因为我们需要计算一次出现的串,而某一重复前缀必定受到上述限制,所以这样是正确的。
- 比较子串大小
若要比较
- 如果
,则 为前后缀关系, 等价于 。 - 否则,
,在LCP
中就能比较出来,等价于比较 。
树上 SA
先上题目:P5353 树上后缀排序
大体思路与上文相同,用倍增加基数排序来实现。
我们知道一个字符串的后缀排序每个后缀长度都是相同的,不会出现有两个后缀相同的情况,而树上就很可能发生。
按题目要求说如果两串相同就看父节点的编号大小,所以此处变成有三个关键字的基数排序。
看代码来解释。
显然基数排序是不变的。
void tsort(int *sa, int *rk, int *id, int m)
{
for (int i = 1; i <= m; i++)
cnt[i] = 0;
for (int i = 1; i <= n; i++)
cnt[rk[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
}
既然是用倍增来求解,为了快速求出一个结点向上
for (int j = 1; j <= 19; j++)
for (int i = 1; i <= n; i++)
f[j][i] = f[j - 1][f[j - 1][i]];
然后就进入基数排序,第一次同普通 SA 求出 sa
,rk
,然后要多求一个 rkk
表示:后缀 rk
这里排名不能重复),目的是求父节点的排名。
tsort(sa, rk, id, 122);
rk[sa[1]] = rkk[sa[1]] = 1;
int num = 1, m = 122;
for (int i = 2; i <= n; i++)
{
if (s[sa[i]] == s[sa[i - 1]])
rk[sa[i]] = num;
else
rk[sa[i]] = ++num;
rkk[sa[i]] = i;
}
然后就是倍增排序。
先求父节点的 sa
,第一关键字就是当前结点向上跳 rk2
。第二关键字为子节点的第一关键字,也就是 sa
。
这里做一次基数排序,求出子节点的第二关键字。再做一次基数排序,求出子节点的 sa
。
tsort(id, rk2, sa, n);
tsort(sa, rk, id, m);
剩下操作同普通 SA 相同,也记得要记录 rkk
。
swap(rk, id);
num = 1;
rk[sa[1]] = rkk[sa[1]] = 1;
for (int i = 2; i <= n; i++)
{
if (id[sa[i]] == id[sa[i - 1]] && id[f[t][sa[i]]] == id[f[t][sa[i - 1]]])
rk[sa[i]] = num;
else
rk[sa[i]] = ++num;
rkk[sa[i]] = i;
}
if (num == n)
break;
m = num;
例题
P2870 [USACO07DEC] Best Cow Line G
可以把原串复制翻转一遍,接在原串后面,跑一遍 SA。
走双指针,
!P2178 品酒大会
因为有负值调了好久。
此题又有后缀,有让求两个后缀的 lcp,那很自然的想到 SA。
跑 SA,求 height
。
然后此题要求两个问,我们一个一个求。
有多少对?
此题的询问是让输出
height
只求了
我们把
当
但如果每次都把
此时并查集的的每个集合中要记录 fa
,size
。
每合并一个集合就让
- 所有满足要求的二元组中,乘积最大是多少?
有了上面的的方法只需要在每个集合中记录最大值与次大值即可。
但此题有负数,所以还要记录负数的最小值与次小值。
此题解决。
丑陋的代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 10, inf = 1e18;
int n;
int sa[N], rk[N], cnt[N], id[N];
int a[N];
pair<int, int> ans[N];
char s[N];
struct sim
{
int val, id, to, from;
} h[N];
struct dsu
{
int fa, pmax1, pmax2, nmin1, nmin2, siz;
dsu()
{
pmax1 = pmax2 = -inf;
nmin1 = nmin2 = inf;
}
} bin[N];
void SA()
{
int m = 122;
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 k = 1; k <= n; k <<= 1)
{
int num = 0;
for (int i = n - k + 1; i <= n; i++)
id[++num] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > k)
id[++num] = sa[i] - k;
for (int i = 1; i <= m; i++)
cnt[i] = 0;
for (int i = 1; i <= n; i++)
cnt[rk[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
swap(id, rk);
num = 1;
rk[sa[1]] = 1;
for (int i = 2; i <= n; i++)
{
if (id[sa[i]] == id[sa[i - 1]] && id[sa[i] + k] == id[sa[i - 1] + k])
rk[sa[i]] = num;
else
rk[sa[i]] = ++num;
}
if (n == num)
break;
m = num;
}
}
void get_height()
{
for (int i = 1, k = 0; i <= n; i++)
{
if (rk[i] == 1)
continue;
if (k)
k--;
while (s[i + k] == s[sa[rk[i] - 1] + k])
k++;
h[rk[i]].val = k;
}
}
bool cmp(sim x, sim y)
{
return x.val > y.val;
}
int find(int x)
{
return x == bin[x].fa ? x : (bin[x].fa = find(bin[x].fa));
}
signed main()
{
scanf("%lld", &n);
scanf("%s", s + 1);
SA();
get_height();
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for (int i = 1; i <= n; i++)
{
h[i].id = sa[i];
if (i == 1)
continue;
h[i].from = sa[i - 1];
h[i].to = sa[i];
}
sort(h + 1, h + 1 + n, cmp);
for (int i = 1; i <= n; i++)
{
bin[i].fa = i;
bin[i].siz = 1;
if (a[i] > 0)
bin[i].pmax1 = a[i];
if (a[i] < 0)
bin[i].nmin1 = a[i];
}
int l = 1;
int res = -inf, num = 0;
for (int i = n - 1; i >= 0; i--)
{
while (h[l].val == i && l <= n)
{
int u = h[l].from, v = h[l].to;
int x = find(u), y = find(v);
bin[x].fa = y;
int ss[4], yy[4];
ss[0] = bin[y].nmin1, ss[1] = bin[y].nmin2, ss[2] = bin[x].nmin1, ss[3] = bin[x].nmin2;
yy[0] = bin[y].pmax1, yy[1] = bin[y].pmax2, yy[2] = bin[x].pmax1, yy[3] = bin[x].pmax2;
sort(ss, ss + 4);
sort(yy, yy + 4);
bin[y].pmax1 = yy[3], bin[y].pmax2 = yy[2], bin[y].nmin1 = ss[0], bin[y].nmin2 = ss[1];
int mx1 = bin[y].pmax1, mx2 = bin[y].pmax2, mn1 = bin[y].nmin1, mn2 = bin[y].nmin2;
if (mx1 != -inf && mx2 != -inf)
res = max(res, 1ll * mx1 * mx2);
if (mn1 != inf && mn2 != inf)
res = max(res, 1ll * mn1 * mn2);
if (mn1 != inf && mx1 != -inf)
res = max(res, 1ll * mn1 * mx1);
if (mn1 != inf && mx2 != -inf)
res = max(res, 1ll * mn1 * mx2);
if (mn2 != inf && mx1 != -inf)
res = max(res, 1ll * mn2 * mx1);
if (mn2 != inf && mx2 != -inf)
res = max(res, 1ll * mn2 * mx2);
num += bin[x].siz * bin[y].siz;
bin[y].siz += bin[x].siz;
ans[i].first = num;
l++;
}
ans[i].second = res != -inf ? res : 0;
}
for (int i = 0; i < n; i++)
printf("%lld %lld\n", ans[i].first, ans[i].second);
return 0;
}
*P4248 差异
此题还是利用:
定义 f
为
找到第一个小于
此处就可以用单调栈了。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10;
char s[N];
int n, sa[N], cnt[N], id[N], rk[N], h[N], f[N];
int ans = 0;
stack<int> q;
void SA()
{
int m = 122;
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 k = 1; k <= n; k <<= 1)
{
int num = 0;
for (int i = n - k + 1; i <= n; i++)
id[++num] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > k)
id[++num] = sa[i] - k;
for (int i = 1; i <= m; i++)
cnt[i] = 0;
for (int i = 1; i <= n; i++)
cnt[rk[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
swap(id, rk);
rk[sa[1]] = 1, num = 1;
for (int i = 2; i <= n; i++)
{
if (id[sa[i]] == id[sa[i - 1]] && id[sa[i] + k] == id[sa[i - 1] + k])
rk[sa[i]] = num;
else
rk[sa[i]] = ++num;
}
if (num == n)
break;
m = num;
}
int k = 0;
for (int i = 1; i <= n; i++)
{
if (rk[i] == 1)
continue;
if (k)
k--;
while (s[i + k] == s[sa[rk[i] - 1] + k])
k++;
h[rk[i]] = k;
}
}
signed main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
SA();
ans = (n + 1) * n / 2 * (n - 1);
for (int i = 1; i <= n; i++)
{
while (!q.empty() && h[i] < h[q.top()])
q.pop();
if (q.empty())
f[i] = h[i] * i;
else
f[i] = f[q.top()] + (i - q.top()) * h[i];
q.push(i);
ans -= 2 * f[i];
}
cout << ans << endl;
return 0;
}
P3181 找相同字符
题意翻译就是求串
像上题的解法,用单调栈找到第一个小于
但此题的 f
求法有变化,后缀匹配时不能匹配到原串(
定义一个 sum
数组,记录哪些串属于
可得式子:
最后统计 f
。
code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5 + 10;
int n;
int sa[N], rk[N], cnt[N], id[N], h[N];
char s1[N], s2[N], s[N];
int f[N], q[N], sum[N];
void SA()
{
int m = 128;
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 k = 1; k <= n; k <<= 1)
{
int num = 0;
for (int i = n - k + 1; i <= n; i++)
id[++num] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > k)
id[++num] = sa[i] - k;
for (int i = 1; i <= m; i++)
cnt[i] = 0;
for (int i = 1; i <= n; i++)
cnt[rk[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
swap(id, rk);
num = 1;
rk[sa[1]] = 1;
for (int i = 2; i <= n; i++)
{
if (id[sa[i]] == id[sa[i - 1]] && id[sa[i] + k] == id[sa[i - 1] + k])
rk[sa[i]] = num;
else
rk[sa[i]] = ++num;
}
if (n == num)
break;
m = num;
}
}
void get_height()
{
for (int i = 1, k = 0; i <= n; i++)
{
if (rk[i] == 1)
continue;
if (k)
k--;
while (s[i + k] == s[sa[rk[i] - 1] + k])
k++;
h[rk[i]] = k;
}
}
signed main()
{
scanf("%s", s1 + 1);
scanf("%s", s2 + 1);
int len1 = strlen(s1 + 1), len2 = strlen(s2 + 1);
for (int i = 1; i <= len1; i++)
s[i] = s1[i];
s[len1 + 1] = '~';
for (int i = 1; i <= len2; i++)
s[len1 + i + 1] = s2[i];
n = len1 + len2 + 1;
SA();
get_height();
int ans = 0;
int top = 0;
for (int i = 1; i <= n; i++)
{
if (sa[i] <= len1)
sum[i]++;
sum[i] += sum[i - 1];
}
for (int i = 1; i <= n; i++)
{
while (top && h[i] < h[q[top]])
top--;
f[i] = f[q[top]] + (sum[i - 1] - sum[q[top] - 1]) * h[i];
q[++top] = i;
if (sa[i] > len1 + 1)
ans += f[i];
}
memset(f, 0, sizeof f);
for (int i = 1; i <= n; i++)
{
sum[i] = 0;
if (sa[i] > len1 + 1)
sum[i]++;
sum[i] += sum[i - 1];
}
top = 0;
for (int i = 1; i <= n; i++)
{
while (top && h[i] < h[q[top]])
top--;
f[i] = f[q[top]] + (sum[i - 1] - sum[q[top] - 1]) * h[i];
q[++top] = i;
if (sa[i] <= len1)
ans += f[i];
}
printf("%lld", ans);
return 0;
}
*P1117 [NOI2016] 优秀的拆分
此题求把一个字符串构成 AABB 的方案数。
考虑枚举 AA 与 BB 的分割点,设 分割点前面的 AA 的方案数为数组 a
,分割点前面的 BB 的方案数为数组 b
。
现在的难点就是 a
与 b
的求法。
记两个特殊点
如下图:
蓝串为
蓝红串正反跑一次 SA 可求出。
当
把头尾标记下来跑一次前缀和就求出了 a
,b
。
code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int T,h1[N],h2[N],sa[N],cnt[N],id[N],rk[10][N],a[N],b[N],st1[N][30],st2[N][30];
char s[N];
void clearr()
{
memset(a,0,sizeof a);
memset(b,0,sizeof b);
memset(st1,0,sizeof st1);
memset(st2,0,sizeof st2);
memset(sa,0,sizeof sa);
memset(id,0,sizeof id);
}
void SA(int idd)
{
int m=122,n=strlen(s+1);
for(int i=1;i<=m;i++)cnt[i]=0;
for(int i=1;i<=n;i++)cnt[rk[idd][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[idd][i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
int num=0;
for(int i=n-k+1;i<=n;i++)id[++num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>k)id[++num]=sa[i]-k;
for(int i=1;i<=m;i++)cnt[i]=0;
for(int i=1;i<=n;i++)cnt[rk[idd][id[i]]]++;
for(int i=2;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[idd][id[i]]]--]=id[i],id[i]=0;
swap(rk[idd],id);
rk[idd][sa[1]]=1,num=1;
for(int i=2;i<=n;i++)
{
if(id[sa[i]]==id[sa[i-1]]&&id[sa[i]+k]==id[sa[i-1]+k])
rk[idd][sa[i]]=num;
else rk[idd][sa[i]]=++num;
}
if(n==num)break;
m=num;
}
}
int st(int a,int b,int id)
{
int l=rk[id][a],r=rk[id][b];
if(l>r)swap(l,r);
l++;
int t=log2(r-l+1);
if(id==1)
return min(st1[l][t],st1[r-(1<<t)+1][t]);
if(id==2)
return min(st2[l][t],st2[r-(1<<t)+1][t]);
}
signed main()
{
scanf("%lld",&T);
while(T--)
{
memset(rk[1],0,sizeof rk[1]);
memset(rk[2],0,sizeof rk[2]);
memset(h1,0,sizeof h1);
memset(h2,0,sizeof h2);
scanf("%s",s+1);
int n=strlen(s+1);
clearr();
SA(1);
int k=0;
for(int i=1;i<=n;i++)
{
if(rk[1][i]==1)continue;
if(k)k--;
while(s[i+k]==s[sa[rk[1][i]-1]+k])k++;
h1[rk[1][i]]=k;
}
reverse(s+1,s+1+n);
clearr();
SA(2);
k=0;
for(int i=1;i<=n;i++)
{
if(rk[2][i]==1)continue;
if(k)k--;
while(s[i+k]==s[sa[rk[2][i]-1]+k])k++;
h2[rk[2][i]]=k;
}
for(int i=1;i<=n;i++)
st1[i][0]=h1[i],st2[i][0]=h2[i];
for(int i=1;i<=18;i++)
for(int j=1;j+(1<<i)-1<=n;j++)
{
st1[j][i]=min(st1[j][i-1],st1[j+(1<<(i-1))][i-1]);
st2[j][i]=min(st2[j][i-1],st2[j+(1<<(i-1))][i-1]);
}
for(int len=1;len<=n/2;len++)
{
for(int i=len;i<=n;i+=len)
{
int j=i+len;
int lcp=min(len,st(i,j,1));
int lcs=min(len-1,st(n-(i-1)+1,n-(j-1)+1,2));
if(lcs+lcp>=len)
{
a[i-lcs]++,a[i-lcs+(lcp+lcs-len+1)]--;
b[j+lcp-(lcp+lcs-len+1)]++,b[j+lcp]--;
}
}
}
for(int i=1;i<=n;i++)
a[i]+=a[i-1],b[i]+=b[i-1];
long long ans=0;
for(int i=1;i<n;i++)
ans+=a[i+1]*b[i];
cout<<ans<<endl;
}
return 0;
}
回文自动机(PAM)
学完 ACAM 这个就不难了。
没找到很好的讲解,自己又懒得写(待补坑)。
小性质:以一个点结尾的回文串个数就是它所代表的点在 fail 树上的深度.
模板
code:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
struct PAM
{
int len, fail, son[26], cnt;
} tr[N];
int n, las, tot = 1;
char s[N];
int get_fail(int u, int id)
{
while (id - tr[u].len - 1 <= 0 || s[id - tr[u].len - 1] != s[id])
u = tr[u].fail;
return u;
}
int ins(int v, int pos)
{
int p = get_fail(las, pos);
if (!tr[p].son[v])
{
tr[++tot].fail = tr[get_fail(tr[p].fail, pos)].son[v];
tr[p].son[v] = tot;
tr[tot].len = tr[p].len + 2;
tr[tot].cnt = tr[tr[tot].fail].cnt + 1;
}
las = tr[p].son[v];
return tr[tr[p].son[v]].cnt;
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
tr[0].fail = 1, tr[1].len = -1;
int la = 0;
for (int i = 1; i <= n; i++)
{
if (i > 1)
s[i] = (s[i] - 'a' + la) % 26 + 'a';
la = ins(s[i] - 'a', i);
printf("%d ", la);
}
return 0;
}
例题
P3649 [APIO2014] 回文串
PAM 同其他自动机一样,可以求出所有本质不同子串,且只有 N 个,那么本题实际上就是要统计每种回文串的出现次数。
可以在构建 PAM 的时候额外维护一个 cnt 数组,表示每个 PAM 节点对应多少个位置的最长回文后缀,也是每个点有多少次成为了 Last 节点。
这样每个节点代表的回文串的出现次数等于,Fail 树子树的和。
这里是 DAG 可以用拓扑排序来求子树和,但 PAM 与 SAM 一样结点编号就是拓扑序的倒序(ACAM 不是),直接遍历即可。
for (int i = tot; i >= 1; i--)
{
tr[tr[i].fail].cnt += tr[i].cnt;
ans = max(ans, 1ll * tr[i].len * tr[i].cnt);
}
CF17E
这里只探究 PAM 的解法,具体解法看下章中的 Palindrome Series,这里只讲邻接表存储 PAM 等自动机。
此题数据范围为:son
的
然后写一个函数暴力沿邻接表找到子节点。
int get_son(int u, int v)
{
for (int e = lin[u].las; e; e = lin[e].nxt)
{
if (lin[e].val == v)
return lin[e].to;
}
return -1;
}
这样空间复杂度可保证,时间复杂度最大为
border
border 在 KMP 中已经出现,这里做一个 border 性质的小总结。
定义:如果字符串 S 的同长度的前缀和后缀完全相同,即 Prefix[i] = Suffix[i]
则称此前缀(后缀)为一个 Border(根据语境,有时 Border 也指长度)。
KMP 有关
P4391 [BOI2009] Radio Transmission 无线传输
最短周期:
证明:
我们知道
如下图:
蓝色的与红色的两个字串相同,并为最长的公共前后缀。
橙色与红1相同,红1与蓝1相同,所以橙色与蓝1相同,依次类推,可证橙色一定是字符串的一个周期。
而
双倍经验:Power Strings
P3435 [POI2006] OKR-Periods of Words
很好的利用了
先阐述一下题目意思:定义一个
就以整个
我们分为两种情况:
- 字符串的
长度小于这个字符串长度的一半。
先只看那一个红色的字串。
两个紫色的字串为红色大串的最长公共前后缀。
黑色框中的字串就为一个周期,把它设为
看下面的灰色的串就是复制后的
- 字符串的
长度小于这个字符串长度的一半。
现在看最大的黑串。
红色的字串与蓝色的字串相同,为最长公共前后缀;
但我们套用上一个的结论似乎不成立,因为橙色的串复制后根本达不到黑串的长度。
但我们可以发现如果用整串减去红串的最长公共前后缀,为黑串的一个周期。
因为紫串等于三个绿串,所以粉框中的字串就为黑串的周期。
但此时求出的不一定的最小周期,所以我们一直 j=nxt[j]
直到:nxt[j]==0
时为止。
这样即求出最小周期,也把上面两种情况合并了。
如果每一个串都跳的话时间复杂度太高,所以递推
code:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
char s[N];
int p[N], n;
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
p[1] = 0;
for (int i = 2, j = 0; i <= n; i++)
{
while (j && s[j + 1] != s[i])
j = p[j];
if (s[j + 1] == s[i])
j++;
p[i] = j;
}
// for (int i = 1; i <= n; i++)
// cout << p[i] << " ";
// cout << endl;
int j = 2;
long long ans = 0;
for (int i = 2; i <= n; i++)
{
j = i;
while (p[j])
j = p[j];
if (p[i])
p[i] = j;
ans += i - j;
}
printf("%lld", ans);
return 0;
}
PAM 有关(Palindrome Series)
后言
此文章只是涵盖了基础知识点及简单扩展,还有很多字符串与数据结构,动态规划等的结合,在此就不一一阐述。
此外还有后缀自动机,广义后缀自动机,后缀平衡树等算法,可以自行学习(SAM 在 oi wiki 与其他博客中已经有很完善的算法讲解与拓展应用了)。
update:2024.05.15 添加树上 SA。
update:2024.05.17 添加PAM,border 的一部分应用。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战