字符串可以说是我比较薄弱的一块(想当初c++入门的时候就没有系统的学过字符串的读入。。。)
KMP
KMP算法是一种改进的字符串匹配算法
用于解决询问串中出现了多少次模式串
可以说是字符串中最简单的一种算法了
匹配串的时候,如果匹配成功,模式串指针也要跳到失配上,便于下一步的匹配
//字符串下标从0开始
int t[N];
void KMP() {
t[0]=-1;
int len=strlen(s);
for (int i=0;i<len;i++) {
int p=t[i];
while (p!=-1&&s[p]!=s[i]) p=t[p];
t[i+1]=p+1;
}
}
void solve() {
int len=strlen(S),l=strlen(s);
int i,j=0;
for (int i=0;i<len;i++) {
while (S[i]!=s[j]&&j!=-1) j=t[j];
j++;
if (j==l) printf("%d\n",i-l+1),j=t[j];
}
}
Manacher
manacher详解
manacher算法,计算以每个字符为中心的最长回文串
裸题不多,但是和其他算法的结合难度不小
经典例题:manacher+FFT
难题:manacher+双hash
//字符串下标从1开始
const int N=100010;
char s[N],ss[N<<1];
int RL[N];
int prepare() {
int len=strlen(ss+1);
s[0]='@';
for (int i=1;i<=len;i++) {
s[2*i-1]='#';
s[2*i]=ss[i];
}
s[2*len+1]='#';
s[2*len+2]='$';
return 2*len+1;
}
void manacher() {
int len=prepare();
int mx=0,pos=0;
for (int i=1;i<=len;i++) {
int j=pos*2-i;
if (i<mx) RL[i]=min(mx-i,RL[j]);
else RL[i]=1; //如果i>=mx,要从头开始匹配
while (s[i+RL[i]]==s[i-RL[i]]) RL[i]++;
if (i+RL[i]>mx) {
mx=i+RL[i];
pos=i;
}
}
}
AC自动机
Aho-Corasick automaton,著名的多模匹配算法
前面介绍的KMP算法可以解决字符串匹配的问题,然而如果模式串有多个,我们就需要一种全新的算法:AC自动机
因为AC自动机的题本来就做的不多,所以还是多看看题(蓝皮上的讲解还是不错的)
经典例题:AC+fail
经典例题:AC+dp(一)
经典例题:AC+dp(二)
经典例题:AC+dp+矩阵加速(一)
经典例题:AC+dp+矩阵加速(二)
经典例题:AC+dp+矩阵加速(三)
AC自动机:给出字典集和一个长串,询问长串中每个模式串出现的次数
const int N=100010;
int ch[N][26],sz=0,n,m,root=0,fail[N];
int ed[N],word[N],cnt[N],q[N];
char s[N];
void insert(int bh) {
int len=strlen(s);
int now=root;
for (int i=0;i<len;i++) {
int x=s[i]-'a';
if (!ch[now][x]) ch[now][x]=++sz;
now=ch[now][x];
}
ed[now]=1;
word[bh]=now; //记录单词的ed
}
void make_fail() {
int tou,wei;
tou=wei=0;
for (int i=0;i<26;i++)
if (ch[root][i])
q[++wei]=ch[root][i];
while (tou<wei) {
int now=q[++tou];
for (int i=0;i<26;i++) {
if (!ch[now][i]) {
ch[now][i]=ch[fail[now]][i];
continue;
}
fail[ch[now][i]]=ch[fail[now]][i];
ed[ch[now][i]]|=ed[fail[ch[now][i]]];
q[++wei]=ch[now][i];
}
}
}
void solve() {
int len=strlen(s);
int now=root;
for (int i=0;i<len;i++) {
int x=s[i]-'a';
while (now&&!ch[now][x]) now=fail[now];
now=ch[now][x];
cnt[now]++;
int f=fail[now];
while (f) { //沿着fail跑
cnt[f]++; f=fail[f];
}
}
//cnt[word[i]]就是模式串i在长串中的出现次数
}
void build() {
scanf("%d",&n);
for (int i=1;i<=n;i++) {
scanf("%s",s);
insert(i);
}
make_fail();
scanf("%s",s);
solve();
}
上面给出的是比较暴力的做法,还有一种比较优美的做法
我们每次只在匹配时经过的结点 的cnt++,最后利用make_fail时得到的bfs序,逆序累加到fail上即可
AC自动机:给出字典集和一个长串,询问长串中出现了多少模式串
void solve() {
int len=strlen(s);
int now=root;
for (int i=0;i<len;i++) {
int x=s[i]-'a';
while (now&&!ch[now][x]) now=fail[now];
now=ch[now][x];
int f=now;
while (f&&!cnt[f]) {
cnt[f]++;
f=fail[f];
}
}
int ans=0;
for (int i=1;i<=n;i++) if (cnt[word[i]]) ans++;
printf("%d\n",ans);
}
Trie树
怎么说呢,感觉就是失去的fail指针的AC自动机
感觉很废,然而有些题目不能在fail指针上乱跑,这个时候就必须用到Trie了
Trie树的一个经典题目就是:
给出一个字符串集合S,定义P(S)为所有字符串的公共前缀长度*|S|
给定n个字符串,从中选出一个集合S,使P(S)最大
解:建出Trie树,那么Trie上的任何一个结点到根结点的路径就是若干字符串的LCP
我们字需要dfs一遍,计算
max(deep[i]∗cnt[i])
m
a
x
(
d
e
e
p
[
i
]
∗
c
n
t
[
i
]
)
其中
cnt[i]
c
n
t
[
i
]
表示
i
i
子树中的叶节点数量(每个叶结点都代表一个字符串)
在构造Trie的时候,由于结点数较多,我们在存储的时候用的是左儿子右兄弟法
struct Trie{
int son[N]; //左儿子
int nxt[N]; //右兄弟
char ch[N]; //第i个结点的字符
int cnt[N]; //子树的叶结点数 即该子树内包含的不同字符串个数
int sz=0;
void init() {
memset(son,0,sizeof(son));
memset(nxt,0,sizeof(nxt));
memset(cnt,0,sizeof(cnt));
sz=0;
}
void insert(char *s) {
int pre=0,now;
int len=strlen(s);
cnt[0]++; //
for (int i=0;i<len;i++) {
bool ff=0;
for (now=son[pre];now;now=nxt[now])
if (ch[now]==s[i]) {ff=1;break;}
if (!ff)
{
now=++sz;
ch[now]=s[i];
nxt[now]=son[pre];
son[pre]=now;
}
pre=now; //
cnt[pre]++;
}
}
void dfs(int now,int dep) {
if (son[now]==0) ...
else {
...
for (int i=son[now];i;i=nxt[i]) {
...
dfs(i,dep+1);
...
}
...
}
}
};
后缀数组
后缀数组的原理一句两句话是说不清楚的
不过SA的应用可以说是变化多端(dada的总结)
单个字符串相关问题
常见思路:构造后缀数组,然后求height数组,用这两个来求解
重复子串问题
可重叠的最长重复子串问题
因为可重复,所以这类问题比较简单,只需要求数组的最大值
因为
height[i]
h
e
i
g
h
t
[
i
]
表示排名为
i
i
和的后缀的LCP,只需要最大值即可
时间复杂度是线性的
不可重叠的最长重复子串问题
有了不可重叠的限制稍微复杂一点,要用到
height
h
e
i
g
h
t
数组的性质
二分答案,将问题转化为判定性问题
假设我们二分的长度为
k
k
,在SA中相近的字符串肯定会挨在一起
所以我们把连续一段的后缀划分成一段,
如果有某一段满足段中最大的SA值与最小值之差大于等于
k
k
(在字符串中的位置相差至少k)
那么当前解就是可行的,满足条件一定不重叠
注意:这种分段的思想在后缀数组相关问题中很常用
可重叠的 k 次最长重复子串
例:(JZOJ2265. 【Usaco DEC06 Gold】Milk Patterns)给定一个字符串,求至少出现k次的最长重复子串。
先二分答案,只需判断当前段内是否出现个后缀即可
子串计数问题
重复出现子串计数问题
例:(JZOJ1598. 文件修复)求一个字符串中有多少个至少出现两次的子串
这是比较简单的SA题,设每个后缀
rank
r
a
n
k
为
i
i
,其最多能贡献个不同重复子串
∴Ans=∑max(height[i]−height[i−1],0)
∴
A
n
s
=
∑
m
a
x
(
h
e
i
g
h
t
[
i
]
−
h
e
i
g
h
t
[
i
−
1
]
,
0
)
不相同子串计数问题
例:(spoj694,spoj705)给定一个字符串,求不相同的子串的个数
和上面思路大相径庭
每个后缀
k
k
会产生个前缀,但是会重复计数,所以要减去前面相同的前缀
最后就是
n−sa[k]+1−height[k]
n
−
s
a
[
k
]
+
1
−
h
e
i
g
h
t
[
k
]
个子串。
字典序第K子串问题
例:(JZOJ2824. 【GDOI2012】字符串)给出一个字符串S,问该字符串的所有不同的子串中,按字典序排第K的字串
应该勉强算计数问题吧。。。其实就是不相同子串个数的扩展
算出每个后缀贡献的不同子串个数
n−sa[i]+1−height[i]
n
−
s
a
[
i
]
+
1
−
h
e
i
g
h
t
[
i
]
,在二分找出最终子串位置(必然是某个后缀的前缀)
连续重复子串问题
连续重复子串问题
例:给定一个字符串 L,已知这个字符串是由某个字符串S重复R次而得到的, 求R的最大值
比较简单的重复子串问题。枚举串
S
S
长度,如果
rank[1]
r
a
n
k
[
1
]
和
rank[k+1]
r
a
n
k
[
k
+
1
]
的
height=|L|−k
h
e
i
g
h
t
=
|
L
|
−
k
那么当前答案合法
(
rank[i]
r
a
n
k
[
i
]
表示后缀
i
i
的排名)
这实际上就是求字符串最小循环节
用KMP算法一秒出解:
重复次数最多的连续重复子串问题
例:给定一个字符串L,求重复次数最多的连续重复子串
还是枚举子串长度
k
k
,看这个长度对应每个位置(即)之间的LCP是否等于
k
k
最长能扩到哪里,就是重复出现次数
多个字符串相关问题
常见做法:
将多个串连在一起,并且中间插入不同且没出现过的字符隔开
这种题比较多变,给出一些经典模型
一个字符串在所有字符串中出现次数问题
例:(JZOJ3258. 【TJOI2013】单词)给定N个字符串,求每个字符串在所有字符串中出现的次数
对于当前字符串,设其在总串中起始位置为
ST[i]
S
T
[
i
]
,那么我们在
height
h
e
i
g
h
t
上二分区间
[1,rank[ST[i]]],[rank[ST[i]]+1,n]
[
1
,
r
a
n
k
[
S
T
[
i
]
]
]
,
[
r
a
n
k
[
S
T
[
i
]
]
+
1
,
n
]
内满足
height≥|S|
h
e
i
g
h
t
≥
|
S
|
,这之间的后缀数量就是答案
SA解法直接把我弄蒙了,其实这道题就是AC自动机的入门题,计算每个单词在字典集中的出现次数
字符串子串重复出现k次问题
例:(JZOJ3975. 串)给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串(注意包括本身)
SA+线段树维护
其他相关问题
字符串不同种连续子串问题
例:(JZOJ4473. 生成魔咒)给定n个操作,每个操作在字符串S尾插入一个字符,求当前操作后共有多少不同种连续子串
仔细分析,这题要减去前面的操作对当前影响(即重复的连续子串)。这个应该算是“前缀数组”吧。具体来说就是将字符串翻转后求一边SA,此时所得就是原串的前缀数组。然后在线段树维护一下。
int sa[N],hei[N],rak[N],wx[N],wy[N],cc[N];
int cmp(int *y,int a,int b,int k) {
int ra1=y[a];
int rb1=y[b];
int ra2=(a+k>=len)? -1:y[a+k];
int rb2=(b+k>=len)? -1:y[b+k];
return ra1==rb1&&ra2==rb2;
}
void make_sa(int len) {
int i,j,k,m,p;
int *x=wx,*y=wy;
m=26; //字符集
for (i=0;i<m;i++) cc[i]=0;
for (i=0;i<len;i++) cc[x[i]=s[i]-'a']++;
for (i=1;i<m;i++) cc[i]+=cc[i-1];
for (i=len-1;i>=0;i--) sa[--cc[x[i]]]=i;
for (k=1;k<=len;k<<=1) {
p=0;
for (i=len-k;i<len;i++) y[p++]=i;
for (i=0;i<len;i++) if (sa[i]>=k) y[p++]=sa[i]-k;
for (i=0;i<m;i++) cc[i]=0;
for (i=0;i<len;i++) cc[x[y[i]]]++;
for (i=1;i<m;i++) cc[i]+=cc[i-1];
for (i=len-1;i>=0;i--) sa[--cc[x[y[i]]]]=y[i];
swap(x,y);
x[sa[0]]=0;
p=1;
for (int i=1;i<len;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],k)? p-1:p++;
if (p>=len) break;
m=p;
}
}
void make_hei(int len) {
for (int i=0;i<len;i++) rak[sa[i]]=i;
hei[0]=0;
int k=0;
for (int i=0;i<len;i++) {
if (!rak[i]) continue;
int j=sa[rak[i]-1];
if (k) k--;
while (s[i+k]==s[j+k]&&i+k<len&&j+k<len) k++;
hei[rak[i]]=k;
}
}
SAM
对于自己不清楚的算法,不敢胡言乱语,一下总结的还是比较优美的
这种算法算是比较困难的了,还是需要好好理解
感觉曲神的总结还是挺好的
有一些真的点醒了我:
我们每次新建立了一个状态,之后还有两件事要干:找出能转移到这个状态的状态,建立链接;确定这个状态的
min
m
i
n
,即找到ta在
parent
p
a
r
e
n
t
树上的父亲
代码中的
dis
d
i
s
实际上就是指
max
m
a
x
,即达到此状态的最长路径(SAM上的每一条路径都代表着一个子串)
dis[now] d i s [ n o w ] 表示SAM中到这个结点的最长路径(能匹配的最长子串)
匹配的时候 tmp t m p 表示以i为结尾的最长匹配长度
如果我们在匹配的时候能到达 now n o w ,那么一定能到达 fa[now](dis[fa[now]]) f a [ n o w ] ( d i s [ f a [ n o w ] ] )
在构建的时候,我们可以维护一个 size s i z e 数组, size[now]=1 s i z e [ n o w ] = 1
如果我们不想记录重复子串, size[i]=1 s i z e [ i ] = 1
如果我们想记录重复子串, size[fa[i]]+=size[i] s i z e [ f a [ i ] ] + = s i z e [ i ]sum s u m 数组记录的是以 i i 为起点,之后又多少子串
//一个一个字符插入
int root=1,last=1,sz=1;
int ch[N<<1][26],fa[N<<1],dis[N<<1];
void insert(int x) {
int pre=last,now=++sz;
last=now;
dis[now]=dis[pre]+1;
for (;pre&&!ch[pre][x];pre=fa[pre]) ch[pre][x]=now; //找Parent树上有没有这个结点
//维护fa
if (!pre) fa[now]=root;
else {
int q=ch[pre][x]; //找到now可能的fa
if (dis[q]==dis[pre]+1) fa[now]=q;
else {
int nows=++sz;
dis[nows]=dis[pre]+1; //新建一个结点,更新max
memcpy(ch[nows],ch[q],sizeof(ch[q]));
fa[nows]=fa[q]; fa[q]=fa[now]=nows; //更新parent
for (;pre&&ch[pre][x]==q;pre=fa[pre]) ch[pre][x]=nows;
}
}
}
广义后缀自动机
广义后缀自动机,可以解决多个串的问题
可以简单的视为把多个串的SAM合并在了一起
所以广义后缀自动机还有一种暴力一点的构建:每次插入一个新的串前,,之后按照普通后缀自动机的方法构造
空间复杂度还是
O(2n)
O
(
2
n
)
int root=1,sz=1,last=1;
int ch[N<<1][26],fa[N<<1],dis[N<<1];
void insert(int x) {
int now=ch[last][x],pre=last;
if (now) {
if (dis[now]==dis[pre]+1) last=now; //不用新建结点
else {
int nows=++sz;
last=nows;
dis[nows]=dis[pre]+1;
memcpy(ch[nows],ch[now],sizeof(ch[now]));
fa[nows]=fa[now]; fa[now]=nows;
for (;pre&&ch[pre][x]==now;pre=fa[pre]) ch[pre][x]=nows;
}
}
else { //新建结点,正常的SAM构建
now=++sz;
last=now;
dis[now]=dis[pre]+1;
for (;pre&&!ch[pre][x];pre=fa[pre]) ch[pre][x]=now;
if (!pre) fa[now]=root;
else {
int q=ch[pre][x];
if (dis[q]==dis[pre]+1) fa[now]=q;
else {
int nows=++sz;
dis[nows]=dis[pre]+1;
memcpy(ch[nows],ch[q],sizeof(ch[q]));
fa[nows]=fa[q]; fa[q]=fa[now]=nows;
for (;pre&&ch[pre][x]==q;pre=fa[pre]) ch[pre][x]=nows;
}
}
}
}