文章目录
STL
- istringsteam
- s.find()!=string::npos
- s.substr(idx,len)
- s+=“xxx”
- 字符串可以像数字一样有进位处理,如字典序的问题,类似字符串hash 的思想。CF518A
- 用字符数组做映射,可以char s[]=“asdfghjkl”;不用大括号用“”,就不用再内部使用单引号
- istringstream 按任意字符分割串
vector<string> v;
string s;
void split(string x, const char ch) {
v.clear();
istringstream is(x);
string tmp;
while(getline(is, tmp, ch)) {
v.push_back(tmp);
}
}
split(s, '-');
bitset
hdu 5972 puts()、gets()更快!!!
最大最小表示//补充对于两个串的最大最小表示
int minimum(){
int i=0,j=1;
while(i<n&&j<n){
int k;
//一直找到相同的一段
//注意要写(i+k)%n相当于把这个串复制了一遍
for(k=0;k<n;k++){
if(s[(i+k)%n]!=s[(j+k)%n]){
break;
}
}
if(k==n) break;
if(s[(i+k)%n]>s[(j+k)%n]) i=i+k+1;//i+k这一段一定不是最小表示
else j=j+k+1;
if(i==j) j=i+1;
}
return i<j?i:j;
}
int maximum(){
int i=0,j=1;
while(i<n&&j<n){
int k;
//一直找到相同的一段
//注意要写(i+k)%n相当于把这个串复制了一遍
for(k=0;k<n;k++){
if(s[(i+k)%n]!=s[(j+k)%n]){
break;
}
}
if(k==n) break;
if(s[(i+k)%n]>s[(j+k)%n]) j=j+k+1;//i+k这一段一定不是最小表示
else i=i+k+1;
if(i==j) j=i+1;
}
return i<j?i:j;
}
任意子串hash
//字符串要从1开始读
const int key = 137;
unsigned long long H[maxn], xp[maxn]; //64位自然溢出等价于模运算
void Init_Hash(char *s){
int n = strlen(s);
H[0] = 0;
for(int i = 1; i <= n; i++) H[i] = H[i - 1] * key + (s[i] - 'a');
xp[0] = 1;
for(int i = 1; i <= n; i ++) xp[i] = xp[i - 1] * key;
}
unsigned long long Hash(int l, int r){
return H[r] - H[l - 1] * xp[r - l + 1];
}
序列自动机//有更新
处理:b是否为a的子序列、求子序列个数
求next[i][j]为a的第i个字符后面第一次出现字符j的位置
for(int i=n;i;i--){
for(int j=1;j<=a;j++) next[i-1][j]=next[i][j];
next[i-1][s[i]]=i;
}
求b是否为a的代码如下
#include<bits/stdc++.h>
using namespace std;
int nxt[1000006][27],now[27],n;
string s,t;
int main(){
fill(now,now+27,-1);
cin>>s + 1;
for(int i=len;i>=0;i--){
for(int j=0;j<26;j++)
if(i) nxt[i][j]=now[j];//表示第i个字符的下一字母出现的位置(在原串中的下标)
now[s[i]-'a']=i;
}//序列自动机初始化
cin>>n;
while(n--){
cin>>t;
bool flag=true;
len=t.size();
int lac = 0;
if(lac==-1){printf("No\n");}
else {
for(int i=1;i<len;i++){
lac=nxt[lac][t[i]-'a'];
if(lac==-1){
flag=false;
break;
}
}
if(flag)printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
kmp(1 对1匹配,求循环节)
//计算串str的next数组,nxt[i] 表示str[0] ~ str[i-1]的最长相同前后缀
int GETNEXT(char *str)
{
int len=strlen(str);
nxt[0]=nxt[1]=0;//初始化
for(int i=1;i<len;i++)
{
int j=nxt[i];
while(j&&str[i]!=str[j])//一直回溯j直到str[i]==str[j]或j减小到0
j=nxt[j];
nxt[i+1]=str[i]==str[j]?j+1:0;//更新next[i+1]
}
return len;//返回str的长度
}
//返回S串中第一次出现模式串T的开始位置
int KMP(char *S,char *T)
{
int l1=strlen(S),l2=GETNEXT(T);//l2为T的长度
int i,j=0,ans=0;
for(i=0;i<l1;i++)
{
while(j&&S[i]!=T[j])//发生失配则回溯
j=nxt[j];
if(S[i]==T[j])
j++;
if(j==l2)//成功匹配则退出
break;
}
if(j==l2)
return i-l2+1;//返回第一次匹配成功的位置
else
return -1;//若匹配不成功则返回-1
}
//返回第一次x出现在y中的位置
int kmp(int x[],int m,int y[],int n){
kmp_pre(x,m,nxt);
int i,j;
i=j=0;
while(i<n){
while(-1!=j&&y[i]!=x[j]) j=nxt[j];
++i,++j;
if(j>=m){
return i-m+1;
}
}
return -1;
}
//得到串 t 的个数
int KMP_Count(char *S, char *T)
{
int ans=0;
int i,j=0;
int slen = strlen(S), tlen = strlen(T);
if(slen == 1&& tlen == 1){
if(S[0]==T[0])
return 1;
else
return 0;
}
getNext();
for(i = 0; i < slen; i++){
while(j>0&&S[i]!=T[j])
j=next[j];
if(S[i]==T[j])
j++;
if(j==tlen){
ans++;
j = next[j];
}
}
return ans;
}
kmp自动机
将nxt[i]与i连边得到一颗树,树上每个节点都表示s的一个前缀,根到结点x路径上的所有点表示的前缀都是x的border
- 路径上的标号具有单调性,可以二分
- 适合处理单串的公共前后缀border问题
扩展kmp
用途:求T与{S的每一个后缀:s[i~n-1] }的最长公共前缀
主要思想:利用next[j]和extend[i-1]找到S[i~x] 与 T[0~y]相同的部分,从S[x+1]与T[y+1] 开始匹配。
题目:hdu 2449
例: 0 1 2 3 4 5 6
S:a a a a b a a
T:a a a a a
0 1 2 3 4
next[i]:T[i,m-1]和T(T[0~m-1])的最长公共前缀长度 next值为[ 5,4,3,2,1 ] (T本身与T的所有后缀的最长公共前缀长度)
extend[i]:T与S[i,n-1]的最长公共前缀(T与S的所有后缀的最长公共前缀长度)
step1:朴素匹得到extend[0]=4
a. 要求extend[1]:
step2:已知extend[0]=4 -> S[0~3]==T[0~3] -> S[1~3]==T[1~3]
(右端点不可变,为extend[0]-1;左端点可变,往后推至要求的 i )
要求T[1~3]==T[0~?]
则找到next[1]=4 -> T[1~4]==T[0~3] -> T[1~3]==T[0~2]
(左端点不可变,为i和0,右端点可变,往前推至要求的位置T[1~3])
所以S[1~3]==T[0~2] ,从S[4]与T[3]开始匹配,当失配时,记录extend[1]=3=(之前已经匹配的+后来新匹配的=3+0)
b. 要求extend[2]:
step3:已知extend[1]=3 -> S[1~3]==T[0~2] -> S[2~3]==T[1~2]
要求T[1~2]==T[0~?]
则找到next[1]=4 -> T[1~4]==T[0~3] -> T[1~2]==T[0~1]
所以S[2~3]==T[0~1] ,从S[4]与T[2]开始匹配 当失配时,extend[2]=2
c. 要求extend[3]:
step4:已知extend[2]=2 -> S[2~3]==T[0~1] -> S[3~3]==T[1~1]
要求T[1~1]==T[0~?]
则找到next[1]=4 -> T[1~4]==T[0~3] -> T[1~1]==T[0~0]
所以S[3~3]==T[0~0],从S[4]与T[1]开始匹配,当失配时,extend[3]=1
d. 要求extend[4]:
step5:已知extend[3]=1 -> S[3~3]==T[0~0] ->S[4~3]长度为0,说明前面已经匹配的长度为0,则从S[4]与T[0]开始匹配,失配时,extend[4]=0
e. 要求extend[5]:
step6:已知extend[4]=0 ,所以从S[5]与T[0]开始匹配,得到extend[5]=2
f. 要求extend[6]:
step7:已知extend[5]=2 -> S[5~6]==T[0~1] -> S[6~6]==T[1~1]
要求T[1~1]==T[0~?]
则找到next[1]=4 -> T[1~4]==T[0~3] -> T[1~1]==T[0~0]
所以S[6~6]==T[0~0] ,从S[7]与T[1]开始匹配,无S[7],extend[6]=1
const int maxn=100010; //字符串长度最大值
int next[maxn],ex[maxn]; //ex数组即为extend数组
//预处理计算next数组,是原串与原串的exkmp
void GETNEXT(char *str)
{
int i=0,j,po,len=strlen(str);
next[0]=len;//初始化next[0]
while(str[i]==str[i+1]&&i+1<len)//计算next[1]
i++;
next[1]=i;
po=1;//初始化po的位置
for(i=2;i<len;i++)
{
if(next[i-po]+i<next[po]+po)//第一种情况,可以直接得到next[i]的值
next[i]=next[i-po];
else//第二种情况,要继续匹配才能得到next[i]的值
{
j=next[po]+po-i;
if(j<0)j=0;//如果i>po+next[po],则要从头开始匹配
while(i+j<len&&str[j]==str[j+i])//计算next[i]
j++;
next[i]=j;
po=i;//更新po的位置
}
}
}
//计算extend数组
void EXKMP(char *s1,char *s2)
{
int i=0,j,po,len=strlen(s1),l2=strlen(s2);
GETNEXT(s2);//计算子串的next数组
while(s1[i]==s2[i]&&i<l2&&i<len)//计算ex[0]
i++;
ex[0]=i;
po=0;//初始化po的位置:从po开始匹配的位置
for(i=1;i<len;i++)
{
if(next[i-po]+i<ex[po]+po)//第一种情况,直接可以得到ex[i]的值:
ex[i]=next[i-po];
else//第二种情况,要继续匹配才能得到ex[i]的值
{
j=ex[po]+po-i;
if(j<0)j=0;//如果i>ex[po]+po则要从头开始匹配
while(i+j<len&&j<l2&&s1[j+i]==s2[j])//计算ex[i]
j++;
ex[i]=j;
po=i;//更新po的位置
}
}
}
trie树
一般用于处理串的前缀,串加入到Trie中,然后就可以枚举前缀了,一般用来查t是不是一堆字符串的前缀
题目:hdu4760 uvalive 5792
后缀数组(子串是所有后缀的前缀)
后缀数组论文
后缀数组题集参考
题目:
求某两个后缀的最长公共前缀(由性质:给一个字符串,统计该字符串中所有重复出现超过两次的子串的数量,因此转化为求height数组在某一区间内的最小值,rmq问题 )
求可重叠最长重复字串:找height最大值
hdu3518 给一个字符串,统计该字符串中所有重复出现超过两次的子串的数量
求不可重叠最长重复子串:二分答案(子串长度len),每次按len给height分组,检查lcp>=len的组里是否存在两个后缀,长为len的前缀不重叠,即检查该组第一个和最后一个后缀的起始是否不重叠。
==给height分组是,若当前的height [i]不够k,则重新开始 ==
poj 2774 给你两串字符,要你找出在这两串字符中都出现过的最长子串 思路:将两个串拼接起来中间加入分隔符,之后求height数组,找到起始位置分别在左串和右串的最大的height值
hdu 3518 统计该字符串中所有重复出现超过两次的子串的数量
1、重复出现的两个子串之间不能重叠
2、相同的子串只能算一次
思路:枚举子串长度,通过height数组将后缀分组,同一组内都是拥有一定长度的相同前缀。由于 题目要求不重叠,然后找到位置的最大和最小,判断之差是否满足不重叠即可
poj3261 求可重叠最长k-重复子串 思路:二分答案(该子串的长度),根据该长度将后缀数组分组(分的组都是连续的后缀,同一组内都是拥有一定长度的相同前缀,因为排序),每组任意两个后缀的lcp都>=len,因此在组中,长度为len的前缀出现了|group|次,只要找到一个组的个数不小于k即可。
poj 3729 串1中有多少个后缀和 串2中的某个后缀 的lcp 为 k
poj 1226 给定n个字符串,求出现或反转后出现在每个字符串中的最长子串 思路:只需要先将每个字符串都反过来写一遍,中间用一个互不相同的且没有出现在字符串中的字符隔开//保证S的后缀的公共前缀不会跨出一个原有串的范围,再将 n 个字符串全部连起来,中间也是用一个互不相同的且没有出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判断的时候,要看是否有一组后缀在每个原来的字符串或反转后的字符串中出现。
关于看看是不是每一个字符串都出现过有一个比较好的方法:设置数组id[i]表示i这个位置属于几组,然后用bool数组判断就好了
hdu 4029 求有多少个不同的字母子矩阵
思路:暴力,一般思路是n*m地枚举左上角和长与宽,现在通过预处理加速,对每一行求hash[i][j],相当于用一个数值把这连续的几列压缩起来,然后把压缩后的所有列都首尾相接用特殊字符连起来,转化为:求有多少个不同的子串
字符串hash 后范围大,可能有负数,要离散化处理一下
求不同的子串个数,由于子串是所有后缀的所有前缀,所以每个后缀贡献的有效不同子串是从与上一个后缀lcp的下一个字符开始的
spoj Relevant Phrases of Annihilation 给n个字符串,求在每个字符串中出现至少两次且不重叠的最长子串的长度。
spoj New DistinctSubstrings 问该字符串中出现了多少个不同的子串。思路:每个子串都是所有后缀的前缀,因此所有的后缀的贡献都是自己的长度(n-sa[i])-height[i]
ural1297 求最长回文子串,后缀数组O(nlogn)
将字符串反转加在后面,
法一:枚举回文子串开始的位置,则回文子串结束的位置就是sa[i]、sa[i-1]的关系,回文子串的长度就是height[i]的值
int dp[maxn][35];
void RMQ_init(int n){
int m = floor(log(n + 0.0) / log(2.0));//log2(n) = loge(n)/loge(2);
for(int i = 1; i <= n; i++) dp[i][0] = height[i];// 以位置i开始长度为2^0=1的最小值是它本身
for(int i = 1; i <= m; i++) {
for(int j = n; j > 0; j--) {
dp[j][i] = dp[j][i-1];
if(j + (1<<(i-1)) <= n) dp[j][i] = min(dp[j][i], dp[j + (1<<(i-1))][i-1]);
}
}
}
int RMQ_query(int a, int b) {
int m = floor(log(b - a + 1.0) / log(2.0));//因为区间的长度为j−i+1,所以可以取m=log2(j−i+1)。则RMQ(A,i,j)=max{f(i,k),f(j−2k+1,k)}
return min(dp[a][m], dp[b - (1<<m) + 1][m]);
}
int lcp(int a, int b) {
a = Rank[a], b = Rank[b];
if(a > b) swap(a, b);
return RMQ_query(a+1, b);//a+1是因为性质:lcp(a,b)=min{lcp(k-1,k)|i+1<=k<=j}
}
int main(){
//freopen("../result.txt","w",stdout);
ss(a);
int len = strlen(a);
int k = 0;
for(int i = 0; i < len; i++) {
str[k++] = a[i];
}
str[k++] = 1;
for(int i = len - 1; i >= 0; i--) {
str[k++] = a[i];
}
str[k] = 0;
da(str, sa, k+1, 250);
calheight(str, sa, k);
RMQ_init(k);
int ans = 1;
int pos = 0;
for(int i = 0; i < len; i++) {
int res = lcp(i, k - i);//偶数情形
if(2 * res > ans) {
ans = 2 * res;
pos = i - res;
}
res = lcp(i, k - i - 1);//奇数情形
if(2 * res - 1 > ans) {
ans = 2 * res - 1;
pos = i - res + 1;
}
}
for(int i = pos, j = 0; j < ans; j++, i++) {
printf("%c", a[i]);
}
puts("");
return 0;
}
法二:枚举回文子串的中心,RMQ查询sa[i]、sa[2*n - i] 的lcp,即求这个新的字符串的某两个后缀的最长公共前缀
manacher 算法O(n)求最长回文子串
模板:
rank[i]是以str[i]开头的后缀的排名,下标从0开始值有效
sa[i]是排第i的后缀是以哪个位置为开头的,sa[0]为str结尾加的0,sa[1]开始是原串本身,下标从0开始有效
height[i]是排名为i与排名为i-1的后缀的lcp,height[0]=0,height[1]=0//因为1-1=0是与结尾加入的字符,后面就是原字符的结果,一直到n](包含n),height[i]是与sa[i]相关的,因为他们的下标都是排名,存的值都是后缀开头的位置,下标从1开始有效
height数组分组,使得前缀一样的都挨着,是sa[i]和sa[i-1]的关系
RMQ查询结合
lcp性质:lcp(a,b)=min{lcp(k-1,k)|i+1<=k<=j}
//以下为倍增算法求后缀数组
int wa[maxn],wb[maxn],wv[maxn],Ws[maxn];
int cmp(int *r,int a,int b,int l)
{return r[a]==r[b]&&r[a+l]==r[b+l];}
void da(const int *r,int *sa,int n,int m){
int i,j,p,*x=wa,*y=wb,*t;
for(i=0;i<m;i++) Ws[i]=0;
for(i=0;i<n;i++) Ws[x[i]=r[i]]++;
for(i=1;i<m;i++) Ws[i]+=Ws[i-1];
for(i=n-1;i>=0;i--) sa[--Ws[x[i]]]=i;
for(j=1,p=1;p<n;j*=2,m=p){
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0;i<n;i++) wv[i]=x[y[i]];
for(i=0;i<m;i++) Ws[i]=0;
for(i=0;i<n;i++) Ws[wv[i]]++;
for(i=1;i<m;i++) Ws[i]+=Ws[i-1];
for(i=n-1;i>=0;i--) sa[--Ws[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
int sa[maxn],Rank[maxn],height[maxn];
//求height数组
void calheight(const int *r,int *sa,int n){
int i,j,k=0;
for(i=1;i<=n;i++) Rank[sa[i]]=i;
for(i=0;i<n;height[Rank[i++]]=k)
for(k?k--:0,j=sa[Rank[i]-1];r[i+k]==r[j+k];k++);
return;
}
int str[maxn];
char a[maxn];
int main(){
scanf("%s",a);
int n=strlen(a);
int m=0;
for(int i=0;i<n;i++){
str[i]=a[i]+1;
m=max(m,str[i]);
}
str[n]=0;
da(str,sa,n+1,m+1);
calheight(str,sa,n);
for(int i=0;i<=n;i++) printf("%d ",sa[i]);
for(int i=0;i<=n;i++) printf("%d ",Rank[i]);
return 0;
}
01字典树
- 01字典树是一棵最多 32层的二叉树,其每个节点的两条边分别表示二进制的某一位的值为 0 还是为 1. 将某个路径上边的值连起来就得到一个二进制串。
2.节点个数为 1 的层(最高层)节点的边对应着二进制串的最高位。
3.以上代码中,ch[i] 表示一个节点,ch[i][0] 和 ch[i][1] 表示节点的两条边指向的节点,val[i] 表示节点的值。
4.每个节点主要有 4个属性:节点值、节点编号、两条边指向的下一节点的编号。
5.节点值 val为 0时表示到当前节点为止不能形成一个数,否则 val[i]=数值。
6.可通过贪心的策略来寻找与 x异或结果最大的数,即优先找和 x二进制的未处理的最高位值不同的边对应的点,这样保证结果最大。
7.用LL 来存val 因为int不能移动32位,所以要处理成LL
int tol; //节点个数
LL val[32*MAXN]; //点的值
int ch[32*MAXN][2]; //边的值
void init()
{ //初始化
tol=1;
ch[0][0]=ch[0][1]=0;
}
void insert(LL x)
{ //往 01字典树中插入 x
int u=0;
for(int i=32;i>=0;i--)
{
int v=(x>>i)&1;
if(!ch[u][v])
{ //如果节点未被访问过
ch[tol][0]=ch[tol][1]=0; //将当前节点的边值初始化
val[tol]=0; //节点值为0,表示到此不是一个数
ch[u][v]=tol++; //边指向的节点编号
}
u=ch[u][v]; //下一节点
}
val[u]=x; //节点值为 x,即到此是一个数
}
LL query(LL x)
{ //查询所有数中和 x异或结果最大的数
int u=0;
for(int i=32;i>=0;i--)
{
int v=(x>>i)&1;
//利用贪心策略,优先寻找和当前位不同的数
if(ch[u][v^1]) u=ch[u][v^1];
else u=ch[u][v];
}
return val[u]; //返回结果
}
ac自动机(多对1匹配)
题目:hdu3695
用途:给你很多个单词,然后给你一段字符串,问你有多少个单词在这个字符串中出现过 long long也能插入,一位一位的插
解释超清楚!!!
步骤:
- 建立fail树和fail指针(使当前字符失配时跳转到具有最长公共后缀的字符继续匹配),构建fail树的时候,当前的点的fail指针应该是他父亲(的父亲……)的fail指针指向的结点的对应字符的结点。
- 依次匹配
解释:
4. trie树记录的是:i号节点下面连着字母j+‘a’的下一个节点是谁,即节点从0(根节点)开始标号,[节点编号,字母编号]可以唯一确定下一个节点是什么。
5. fail树记录的是:表示输入的字符与当前结点的所有孩子结点都不匹配时(注意,不是和该结点本身不匹配),自动机的状态应转移到的节点,考察同样为当前结点表示的字符(但在别的模式串里)能不能匹配这个新节点的孩子,即:由根结点到该结点所组成的字符序列的所有后缀 和 整个目标字符串集合(也就是整个Trie树)中的所有前缀 两者中最长公共的部分
ac自动机的字典树建好后,叶子结点的下一个和他相同的字符指向它自己,不相同的字符指它父亲的fail指针的对应字符(对照代码getfail())
7. 匹配过程:
模板:
//s串中每个字符只属于一个t串
struct AC {
//注意RE的情况,maxnode=单词数*单词长度+常数
int trie[maxn][26]; //字典树
int cntword[maxn]; //记录该单词出现次数
int fail[maxn]; //失败时的回溯指针
int cnt = 0;//节点编号
int lst[maxn];
void init() {
mm(trie,0);
mm(fail,0);
mm(cntword,0);
mm(lst,0);
cnt=0;
}
int idx(char c){ return c-'A';}
void output(int j) {
if(j) {
printf("%d: %d\n",j,cntword[j]);
output(lst[j]);
}
}
void insertWords(char *s) {
int root = 0;
int len=strlen(s);
for(int i=0; i<len; i++) {
int next = idx(s[i]);
if(!trie[root][next])
trie[root][next] = ++cnt;
root = trie[root][next];
}
cntword[root]++; //当前节点单词数+1
}
void getFail() {
fail[0]=0;
queue <int>q;
for(int i=0; i<26; i++) { //将第二层所有出现了的字母扔进队列
if(trie[0][i]) {
q.push(trie[0][i]);
lst[trie[0][i]] = 0;
}
}
while(!q.empty()) {
int now = q.front();
q.pop();
for(int i=0; i<26; i++) { //查询26个字母
int u=trie[now][i];
if(u) {
q.push(u);
int v=fail[now];
while(v&&!trie[v][i]) {
v=fail[v];
}
fail[u] = trie[v][i];
lst[u] = cntword[fail[u]] ? fail[u] : lst[fail[u]];
} else
trie[now][i] = trie[fail[now]][i];
}
}
}
int query(char *s) {
int now = 0,ans = 0;
int len=strlen(s);
for(int i=0; i<len; i++) { //遍历文本串
now = trie[now][idx(s[i])];
for(int j=now; j && cntword[j]!=-1; j=fail[j]) {
// 一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过).
ans += cntword[j];
cntword[j] = -1; //将遍历后的节点标记,防止重复计算
}
}
return ans;
}
};
AC ac;
//类似路径压缩
if(ch[u][i]) {
f[ch[u][i]]=ch[f[u]][i];
q.push(ch[u][i]);
} else ch[u][i]=ch[f[u]][i];
//另一种写法
class ACAutomaton
{
public:
static const int MAX_N = 10000 * 50 + 5;
//最大结点数:模式串个数 X 模式串最大长度
static const int CLD_NUM = 26;
//从每个结点出发的最多边数,字符集Σ的大小,一般是26个字母
int n; //trie树当前结点总数
int id['z'+1]; //字母x对应的结点编号为id[x]
int fail[MAX_N]; //fail指针
int tag[MAX_N]; //根据题目而不同
int trie[MAX_N][CLD_NUM]; //trie树,也就是goto函数
void init()
{
for (int i = 0; i < CLD_NUM; i++)
id['a'+i] = i;
}
void reset()
{
memset(trie[0], -1, sizeof(trie[0]));
tag[0] = 0;
n = 1;
}
//插入模式串s,构造单词树(keyword tree)
void add(char *s)
{
int p = 0;
while (*s)
{
int i = id[*s];
if ( -1 == trie[p][i] )
{
memset(trie[n], -1, sizeof(trie[n]));
tag[n] = 0;
trie[p][i] = n++;
}
p = trie[p][i];
s++;
}
tag[p]++; //因题而异
}
//构造AC自动机,用BFS来计算每个结点的fail指针,就是构造trie图
void construct()
{
queue<int> Q;
fail[0] = 0;
for (int i = 0; i < CLD_NUM; i++)
{
if (-1 != trie[0][i])
{
fail[trie[0][i]] = 0; //root下的第一层结点的fail指针都指向root
Q.push(trie[0][i]);
}
else
{
trie[0][i] = 0; //这是阶段一中的第2步
}
}
while ( !Q.empty() )
{
int u = Q.front();
Q.pop();
for (int i = 0; i < CLD_NUM; i++)
{
int &v = trie[u][i];
if ( -1 != v )
{
Q.push(v);
fail[v] = trie[fail[u]][i];
tag[u] += tag[fail[u]]; //因题而异,某些题目中不需要这句话
}
else
{ //当trie[u][i]==-1时,设置其为trie[fail[u]][i],就构造了trie图
v = trie[fail[u]][i];
}
}
}
}
//因题而异
//在目标串t中匹配模式串
int solve(char *t)
{
int q = 0, ret = 0;
while ( *t )
{
q = trie[q][id[*t]];
int u = q;
while ( u != 0 )
{
ret += tag[u];
tag[u] = 0;
u = fail[u];
}
t++;
}
return ret;
}
} ac;
//S的每一个字符可以重复出现在T里
struct AC {
//注意RE的情况,maxnode=单词数*单词长度+常数
int trie[maxn][26]; //字典树
int cntword[maxn]; //记录该单词出现次数
int fail[maxn]; //失败时的回溯指针
int cnt = 0;//节点编号
int lst[maxn];
int id;
void init() {
mm(trie,0);
mm(fail,0);
mm(cntword,0);
mm(lst,0);
cnt=0;
id=0;
}
int idx(char c){ return c-'A';}
void output(int j) {
if(j) {
printf("%d: %d\n",j,cntword[j]);
output(lst[j]);
}
}
void insertWords(char *s) {
int root = 0;
int len=strlen(s);
for(int i=0; i<len; i++) {
int next = idx(s[i]);
if(!trie[root][next])
trie[root][next] = ++cnt;
root = trie[root][next];
}
cntword[root]=++id; //当前节点单词数+1
}
void getFail() {
fail[0]=0;
queue <int>q;
for(int i=0; i<26; i++) { //将第二层所有出现了的字母扔进队列
if(trie[0][i]) {
q.push(trie[0][i]);
}
}
while(!q.empty()) {
int now = q.front();
q.pop();
for(int i=0; i<26; i++) { //查询26个字母
int u=trie[now][i];
if(u) {
q.push(u);
int v=fail[now];
while(v&&!trie[v][i]) {
v=fail[v];
}
fail[u] = trie[v][i];
lst[u] = cntword[fail[u]] ? fail[u] : lst[fail[u]];
} else
trie[now][i] = trie[fail[now]][i];
}
}
}
void query(char *s) {
int now = 0;
int len=strlen(s);
for(int i=0; i<len; i++) { //遍历文本串
int c=s[i]-'A';
if(c>=0&&c<26){
now=trie[now][c];
for(int j=now;j&&cntword[j];j=fail[j]) ans[cntword[j]]++;
}else now=0;//!!遇到不在字典树里的字母就重新开始匹配
}
}
};
后缀自动机(SAM)
right(a)是指子串a在原串里多次(0/多次)出现,则它的所有位置的右端点,则z后缀自动机添加a串以后,可以识别的字符串就包括所有右端点开始的原串的所有后缀reg(st(a))
如果子串a和b出现的所有区间的右端点集合是一样的,那么后缀自动机可识别的串也是一样的。
如果读入一个串s,SAM能到达的状态是由一个right集合决定的,而这个right集合又是由right(s)组成的。
right(r)集合是由所有结尾在r位置的子串构成的
3.解释:如果几个子串的endpos是相同的,则其他的串都是最长的串的后缀,eg:aababa 子串ab和b的endpos一样
最长的子串p,它的endpos就是这个类里的endpos
对于一个节点i,根s
1、整个自动机可以接受字符串的所有子串,且自动机为一个有向拓扑图
2、一条到i的路径唯一对应一个子串
3、所有i可以接受的状态互相成后缀关系,且长度连续
4、对于i的父亲rt[i],rt[i]所能接受的所有子串都是i所能接受的所有子串的后缀
构造后缀自动机的时候有一步是判断l[a]+1 == l[b],为的是防止多余状态被自动机接受,如果l[a]+1 == l[b],那么b所能接受的最长子串是合法的,根据3,4可知所有其他状态也都是合法的,否则l[a]+1!=l[b]可能会出现不合法状态,那么新建节点使l[c]=l[a]+1就可以筛出合法状态,而rt指针的修改可以看作是逆序后缀树的压缩边被解压缩出一个节点并添加一个新的叶子节点(即新逆序后缀)
5、统计方式的理解。从后续题目来看有两种统计方法,一是根据转移边dp,二是根据父亲边dp,其实是统计两个不同东西,转移边dp可以统计出有多少子串以此节点接受的状态为前缀,根据父亲边dp可以统计出以该节点接受的最长子串为后缀的子串有多少(逆序后缀树i所代表的是i所接受最长子串)
6、其实字典树也可以建立后缀自动机,考虑我们对于串建的时候,记last是为了找出最后一个的最长子串,而在字典树上就是字典树的父亲,所以按dfs序及父亲就可以建出字典树的后缀自动机
7、(逆序后缀树i所代表的是i所接受最长子串)这句话的意思是,将该串所有的逆序后缀建立一棵后缀树,而自动机中的每个节点i,它在后缀树中一直沿父亲边走向根\所代表的逆序后缀==节点i在自动机中接受到的最长子串
总而言之,后缀自动机兼有两种特性,如果想知道后缀的公共前缀之类,或子串重复次数等等与后缀联系的上的,可以用后缀树的性质,如果与所有子串相关,自动机就比较好用
应用:
- 给定文本T,询问格式如下:给定字符串P,问P是否是T的子串。
对T构造SAM,将P放在上面跑,假设状态——变量v,最初是初始状态T_0.我们沿字符串P给出的路径走,因此从当前状态经转移来到新的状态v。如果在某时刻,当前状态没有要求字符的转移,那么答案就是"no"。如果我们处理了整个字符串P,答案就是"yes"。
显然这一算法将在时间O(length§)内运行完毕。并且,该算法实际上找出了P在文本中出现过的最长前缀——如果模式串使得这些前缀都很短,算法将比处理全部模式串要快得多。
- 给定字符串S,问它有多少不同的子串。
每个节点代表的子串的个数和。 因为S的任意子串都对应自动机中的一条路径。答案就是从初始节点t_0开始,自动机中不同的路径条数。 已知后缀自动机是一张有DAG,我们可以考虑用动态规划计算不同的路径数量。也就是,令d[v]为从状态v开始的不同路径条数(包括长度为零的路径),则有转移:d[v]是v所有后继节点的d值之和加上1.最终答案就是d[t_0]-1(减一以忽略空串)。
- 给定字符串S,求其所有不同子串的总长度。
- 给定字符串S,一系列询问——给出整数K_i,计算S的所有子串排序后的第K_i个。
字典序第k小子串—— 自动机中字典序第k小的路径。 因此,考虑从每个状态出发的不同路径数,我们将得以轻松地确定第k小路径,从初始状态开始逐位确定答案。
- 给定字符串S,找到和它循环同构的字典序最小字符串。
我们将字符串S+S建立后缀自动机。该自动机将包含和S循环同构的所有字符串。从而,问题就简化成了在自动机中找出字典序最小的,长度为length(S)的路径,这很简单:从初始状态开始,每一步都贪心地走,经过最小的转移。
- 给定文本T,询问格式如下:给定字符串P,希望找出P作为子串在文本T中出现了多少次(出现区间可以相交)。
- 给定文本T,询问格式如下:给定字符串P,求P在文本中第一次出现的位置。
- 给定文本T,询问格式如下:给定字符串P,要求给出P在T中的所有出现位置(出现区间可以相交)。
- 给定字符串S和字母表。要求找出一个长度最短的字符串,使得它不是S的子串
- 给定两个字符串S和T。要求找出它们的最长公共子串,即一个字符串X,它同时是S和T的子串。
- 给出K个字符串S_1~S_K。要求找出它们的最长公共子串,即一个字符串X,它是所有S_i的子串。
题目子集:
SPOJ#7258 SUBLEX"Lexicographical Substring Search"
BZOJ#2555 Substring
SPOJ#8222 NSUBSTR"Substrings"
SPOJ#1812 LCS2"Longest Common Substrings II"
BZOJ#3998 弦论
const int maxn=100010;
const int maxc=27;
struct SAM {
int len[maxn<<1], next[maxn<<1][maxc], fa[maxn<<1], L, last;//len是每个节点代表的所有子串里最长的长度
SAM() {
init();
}
void init() {
L = 0;
last = newnode(0,-1);
}
int newnode(int l, int pre) {
fa[L] = pre;
for(int i = 0; i < maxc; i++) next[L][i] = -1;
len[L] = l;
return L++;
}
void build(char *p) {
int n=strlen(p);
for(int i = 0; i < n; i++)
add(p[i]-'a', i);
}
void add(int x,int l) {
int pre = last, now = newnode(len[pre]+1, -1);
last = now;
while(pre != -1 && next[pre][x] == -1) {
next[pre][x] = now;
pre = fa[pre];
}
if(pre == -1) {
fa[now] = 0;
} else {
int bro = next[pre][x];
if(len[bro] == len[pre] + 1) {
fa[now] = bro;
} else {
int fail=newnode(len[pre]+1, fa[bro]);
for(int i = 0; i < maxc; i++) next[fail][i] = next[bro][i];
fa[bro] = fail, fa[now] = fail;
while(pre != -1 && next[pre][x] == bro) {
next[pre][x] = fail;
pre = fa[pre];
}
}
}
}
} A;
说明:
(1)parent树中,一个状态s的父状态fas,满足s是fas的后缀 && fas出现次数最少
eg:当前状态s表示的串为a
fas表示的串为pa
原串为:papalalalalala
a既是pa的后缀也是la的后缀,但是选次数小的pa
(2)sam的转移:== 即u所表示的所有子串加上一个字符后得到的子串,由v表示;不一定v所有的子串都由u转移来==,因此采用增量法构造
考虑已经构造好的sam为|s|-1个字符,位于状态last,对于下一个字符c,新建节点np,转移应当新加入last->np,fa(last)->np……直到发现这个状态已经有了一个字符c的转移,记该状态为p,转移c后为q,此时right(q)会多一个
(3)初始状态的right集合最大,叶子节点的right集合最小=1,即:在当前的right集合里找不到任何一个能转移的位置,就改变right集合的大小继续转移
题集说明:
- 求最长公共子串spoj LCS:将 t 放进 sam 匹配,当失配时相当于要从当前匹配到的串的一个后缀继续开始匹配,等价于在 sam 上跳 fa(u)。先构造A的SAM,然后用A的SAM一次读入B的每一个字符,初始时状态在root处,此时最大匹配数为tmp=0,(这里的最大匹配数是指以当前读入的字符结尾,往前能匹配的最大长度),设当前到达的状态为p,最大匹配数为tmp,读入的字符为x,若p->go[x]!=NULL,则说明可从当前状态读入一个字符x到达下一个状态,则tmp++,p=p->go[x],否则,找到p的第一个祖先s,s->go[x]!=NULL,若s不存在,则说明以x结尾的字符串无法和A串的任何位置匹配,则设tmp=0,p=root。否则,设tmp=s->tmp+1(因为我们不算x的话已经到达了状态p,这说明对于p的任意祖先已经匹配完毕),p=s->go[x]。我们求tmp所达到的最大值即为所求
总结:
(1) sam上一条从初始状态出发的路径对应一个子串
(2) parent树上一个节点能表示的最长的串对应一个前缀/后缀
(3) len(u)表示节点 u能表示的最长串的长度
(4) fa(u)表示节点 u 的后缀链接指向的节点,也就是其在 parent树上的父亲
(5) 表示两个后缀的公共前缀的节点是两个后缀在 parent树上的 lca
(6) R(u)表示节点 u 的 right 集合,sz(u) 表示 R(u)大小,R(u)⊂R(fa(u)),sz(u)<sz(fa(u))
(7) 广义 sam可以理解为一个 sam 维护多个串,每一次插入完一个串后将 tail 指针设为初始状态
回文自动机(PAM)
//hdu 6599
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e5 + 5;
int ans[maxn];
char s[maxn];
struct Pam {
int next[maxn][26];
int fail[maxn];
int len[maxn];// 当前节点表示回文串的长度
int vis[maxn];
int cnt[maxn];
int S[maxn];
vector<int> G[maxn];
int last, n, p;
int newNode(int l) {
memset(next[p], 0, sizeof(next[p]));
len[p] = l;
cnt[p] = 0;
G[p].clear();
return p++;
}
void init() {
n = last = p = 0;
newNode(0);
newNode(-1);
S[n] = -1;
fail[0] = 1;
}
int getFail(int x) {
while (S[n - len[x] - 1] != S[n]) {
x = fail[x];
}
return x;
}
void add(int c) {
S[++n] = c;
int cur = getFail(last);
if (!next[cur][c]) {
int now = newNode(len[cur] + 2);
fail[now] = next[getFail(fail[cur])][c];
next[cur][c] = now;
G[fail[now]].push_back(now);
}
last = next[cur][c];
++cnt[last];
}
void update() {
for (int i = p - 1; i >= 0; i--) {
cnt[fail[i]] += cnt[i];
}
}
void build() {
init();
vis[1] = 1;
for (int i = 0; s[i]; i++) {
add(s[i] - 'a');
}
update();
}
void dfs(int x) {
if (vis[(int) ceil(1.0 * len[x] / 2)]) {
ans[len[x]] += cnt[x];
}
++vis[len[x]];
for (auto v : G[x]) {
dfs(v);
}
--vis[len[x]];
}
} pam;
int main() {
while (~scanf("%s", s)) {
pam.build();
pam.dfs(0);
int len = strlen(s);
for (int i = 1; i <= len; ++i) {
printf("%d", ans[i]);
if (i < len) {
printf(" ");
}
ans[i] = pam.vis[i] = 0;
}
printf("\n");
}
return 0;
}
manacher 算法
不可以把原串单独拿出来看,否则会漏掉偶数情况的解 hdu 5340
O(n)求最长回文子串
p数组有一个性质,那就是p[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*p[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有p[i]个分隔符,剩下p[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为p[i]-1。
int p[maxn];
char aft[maxn];//下标从1开始
char str[maxn];
int manacher(char *s) {
int len = strlen(s);
aft[0] = '@';
for(int i = 0; i < len ; i++) {
aft[2*i+1] = '#';
aft[2*i+2] = s[i];
}
aft[2*len+1] = '#';
int n = 2 * len + 2;
p[0] = p[1] = 1;
int mx = 1, id = 1, ret = 1;
//mx:在i之前的回文串中,延伸至最右端的位置
//id:取得这个最优mx时的字符串下标
for(int i = 2; i < n; i++) {
if(mx > i) p[i] = min(p[2*id-i], mx - i);//2*id-i = id-(i-id) 即为i相对于id的左边位置j
else p[i] = 1;
for(; aft[i - p[i]] == aft[i + p[i]]; p[i]++);//超过mx部分一个一个匹配
//更新mx,id
if(i + p[i] > mx) {
mx = i + p[i];
id = i;
}
ret = max(ret, p[i]);
}
return ret - 1;
}
int main(){
//freopen("../result.txt","w",stdout);
while(~ss(str)) {
int ans = manacher(str);
pd(ans);
}
return 0;
}
hdu 5340 必须按照处理后的#s#串去做,不可以把原串单独拿出来计算,否则会缺少偶数情况的解
ss(a);
fir.clear();
sec.clear();
int len = strlen(a);
manacher(a);
for(int i = 1; i < n; i++) {
if(p[i] == i && i != 1) fir.push_back(p[i]);
if(p[i] + i - 1 == n-1 && i != n-1) sec.push_back(p[i]);
}
int flag = 0;
for(int i = 0; i < fir.size(); i++) {
for(int j = 0; j < sec.size(); j++) {
int l = fir[i] * 2, r = n - sec[j] * 2;
if(l > r) continue;
int pos = (l + r) / 2;
if(p[pos] == 1) continue;//pos 为‘#’且不可能含有长度为偶数的子串
if(p[pos] * 2 - 1 >= r - l + 1) {//p[pos]*2-1 是该回文子串的长度,因为manacher将所有串都换成了奇数长度
flag = 1;
break;
}
}
if(flag) break;
}
if(flag) puts("Yes");
else puts("No");
hdu 4513 要求回文串是左边上升的,则manacher条件增加:
for(; aft[i - p[i]] == aft[i + p[i]] && aft[i - p[i]] <= aft[i - p[i] + 2] ; p[i]++);