复健\(Day9\)
字符串相关算法
\(1.\)最小表示法
最小表示法就是找出字符串\(s\)的循环同构串中字典序最小的那一个
时间复杂度为\(O(n)\)
char s[maxn];
int n;
int get_min(char *s)
{
n=strlen(s+1);
for(int i=1;i<=n;i++) s[n+i]=s[i];
int i=1,j=2,k=0;
while(i<=n&&j<=n)
{
for(k=0;k<n&&s[i+k]==s[j+k];k++);
s[i+k]>s[j+k]?i=i+k+1:j=j+k+1;
if(i==j) j++;//若跳转后两个指针相同,则j++,保证比较的两个字符串不同
}
return min(i,j);
}
模板
https://www.luogu.com.cn/problem/P1368
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 600010
using namespace std;
int s[maxn];
int n;
int get_min()
{
for(int i=1;i<=n;i++) s[i+n]=s[i];
int i=1,j=2,k=0;
while(i<=n&&j<=n)
{
for(k=0;k<n&&s[i+k]==s[j+k];k++);
s[i+k]>s[j+k]?i=i+k+1:j=j+k+1;
if(i==j) j++;
}
return min(i,j);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
int start=get_min();
for(int i=start;i<=start+n-1;i++) printf("%d ",s[i]);
return 0;
}
\(2.\)字符串哈希
把不同的字符串映射成不同的整数
对于一个长度为\(n\)的字符串\(s\),这样定义\(Hash\)函数:\(h(s)=\sum_{i=1} ^{n} s[i]\times p^{n-i} (modM)\)
如\(abc\)哈希值为\(a\times p^2+b\times p+c\)
其中,\(p\)为一个质数,通常取其为\(131\)或者\(13331\),\(M\)通常取大整数\(2^{64}\),把哈希函数值\(h\)定义为\(NULL\),超过则自动溢出,等价于取模
typedef unsigned long long ULL
const int P=131;
ULL p[maxn],h[maxn];
void init()//预处理hash函数的前缀和
{
p[0]=1,h[0]=0;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*P;;
h[i]=h[i-1]*P+s[i];
}
}
ULL get(int l,int r)//计算 s[l~r]的hash值
{
return h[r]-h[l-1]*p[r-l+1];
}
bool substr(int l1,int r1,int l2,int r2)//判断两子串是否相等
{
return get(l1,r1)==get(l2,r2);
}
模板
https://www.luogu.com.cn/problem/P3370
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<set>
#define maxn 10010
#define N 1500
#define ULL unsigned long long
using namespace std;
const int P=131;
ULL p[maxn],h[maxn];
set<ULL> S;
void init(char *s)
{
p[0]=1,h[0]=0;
for(int i=1;i<=N;i++)
{
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+s[i];
}
}
ULL get(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
char s[N];
cin>>s;
init(s+1);
int len=strlen(s+1);
S.insert(get(1,len));
}
printf("%d\n",S.size());
return 0;
}
\(3.KMP\)算法
给定一个模式串\(P\)和一个主串\(S\),求模式串\(P\)再主串\(S\)中出现的位置(字符串下标均从\(1\)开始)
\(next[i]\)表示模式串\(P[1,i]\)中相等前后缀的最长长度
使用双指针计算\(ne\)数组
ne[1]=0;
for(int i=2,=0;i<=n;i++)
{
while(j&&P[i]!=P[j+1]) j=ne[j];//若P[i]!=P[j+1],让j回跳到能匹配的位置
if(P[i]==P[j+1]) j++;//指向匹配前缀的末尾
ne[i]=j;
}
模式串与主串匹配
for(int i=1,j=0;i<=m;i++)
{
while(j&&S[i]!=P[j+1]) j=ne[j];
if(S[i]==P[j+1]) j++;
if(j==n) printf("%d\n",i-n+1);//输出匹配开始的位置
}
模板
https://www.luogu.com.cn/problem/P3375
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000010
using namespace std;
int ne[maxn];
char S[maxn],P[maxn];
int main()
{
cin>>(S+1);
cin>>(P+1);
int m=strlen(S+1);
int n=strlen(P+1);
ne[1]=0;
for(int i=2,j=0;i<=n;i++)
{
while(j&&P[i]!=P[j+1]) j=ne[j];
if(P[i]==P[j+1]) j++;
ne[i]=j;
}
for(int i=1,j=0;i<=m;i++)
{
while(j&&S[i]!=P[j+1]) j=ne[j];
if(S[i]==P[j+1]) j++;
if(j==n) printf("%d\n",i-n+1);
}
for(int i=1;i<=n;i++) printf("%d ",ne[i]);
return 0;
}
\(4.\)扩展\(KMP\)算法(\(Z\)函数)
\(Z\)函数:对于一个长度为\(n\) 的字符串\(S\),\(z[i]\)表示\(S\)与其后缀\(S[i,n]\)的最长公共前缀\((LCP)\)的长度
\(Z-box:\)对于\(i\),我们称区间\([i,i+z[i]-1]\)是\(i\)的匹配串,也可以叫\(Z-box\)
void get_z(char *s,int n)//s与s的每一个后缀的LCP的长度数组z
{
z[1]=n;
for(int i=2,l,r=0;i<=n;i++)//维护盒子s[l~r],s[l~r]=s[1~r-l+1]
{
if(i<=r) z[i]=min(z[i-l+1],r-i+1);//若i在盒子内,则s[i~r]=s[i-l+1~r-l+1]
while(s[1+z[i]]==s[i+z[i]]) z[i]++;//这是(z[i-l+1]>=r-i+1之后)或者(i>r即在盒外)的暴力枚举
if(i+z[i]-1>r) l=i,r=i+z[i]-1;//求出z[i]后,如果i+z[i]-1>r,则更新盒子l=i,r=i+z[i]-1
}
}
void get_p(char *s,int n,char *t,int m)//s与t的每一个后缀的LCP的长度数组
{
for(int i=1,l,r=0;i<=m;i++)
{
if(i<=r) p[i]=min(z[i-l+1],r-i+1);
while(1+p[i]<=n&&i+p[i]<=m&&s[1+p[i]]==t[i+p[i]]) p[i]++;
if(i+p[i]-1>r) l=i,r=i+p[i]-1;
}
}
模板
https://www.luogu.com.cn/problem/P5410
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e7+5;
int z[maxn],p[maxn];
char s[maxn],t[maxn];
long long get_ans(int *s,int n)
{
long long ans=0;
for(int i=1;i<=n;i++)
{
ans^=1LL*i*(s[i]+1);
}
return ans;
}
void get_z(char *s,int n)
{
z[1]=n;
for(int i=2,l,r=0;i<=n;i++)
{
if(i<=r) z[i]=min(z[i-l+1],r-i+1);
while(s[1+z[i]]==s[i+z[i]]) z[i]++;
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
void get_p(char *s,int n,char *t,int m)
{
for(int i=1,l,r=0;i<=m;i++)
{
if(i<=r) p[i]=min(z[i-l+1],r-i+1);
while(1+p[i]<=n&&i+p[i]<=m&&s[1+p[i]]==t[i+p[i]]) p[i]++;
if(i+p[i]-1>r) l=i,r=i+p[i]-1;
}
}
int main()
{
cin>>(t+1);
cin>>(s+1);
int n=strlen(s+1);
int m=strlen(t+1);
get_z(s,n);//注意这里是s
get_p(s,n,t,m);//注意这里是s,t
printf("%lld\n",get_ans(z,n));
printf("%lld\n",get_ans(p,m));
}
\(5.\)\(Manacher\)(马拉车)
可以在\(O(n)\)时间内求出一个字符串中的最长回文串
首先要改造字符串:在字符之间和串两端插入\(\#\),改造后,都变成奇回文串
scanf("%s",a+1);
int n=strlen(a+1),k=0;
s[0]='$',s[++k]='#';//s[0]='$',是哨兵(边界)
for(int i=1;i<=n;i++) s[++k]=a[i],s[++k]='#';
n=k;
回文半径\(d[i]\):以\(i\)为中心的最长回文串的长度的一半
加速盒子\([l,r]\)(与扩展\(KMP\)算法中的盒子是一样的):算法过程中我们要维护右端点最靠右的最长回文串,而利用盒子就可以借助之前的状态来加速计算新的状态
void get_d(char *s,int n)
{
d[1]=1;
for(int i=2,l,r=0;i<=n;i++)
{
if(i<=r) d[i]=min(d[r-i+l],r-i+1);
while(s[i-d[i]]==s[i+d[i]]) d[i]++;
if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;
}
}
原串的最长回文串就是新船的最大半径\(-1\)
模板
https://www.luogu.com.cn/problem/P3805
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=3e7;
char a[maxn],s[maxn];
int d[maxn];
void get_d(char *s,int n)
{
d[1]=1;
for(int i=2,l,r=0;i<=n;i++)
{
if(i<=r) d[i]=min(d[r-i+l],r-i+1);
while(s[i+d[i]]==s[i-d[i]]) d[i]++;
if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;
}
}
int main()
{
cin>>(a+1);
int n=strlen(a+1);
int k=0;
s[0]='$',s[++k]='#';
for(int i=1;i<=n;i++) s[++k]=a[i],s[++k]='#';
n=k;
get_d(s,n);
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,d[i]);
printf("%d\n",ans-1);
return 0;
}
\(6.\)字典树\((Trie)\)
\(Trie\)维护字符串的集合,支持两种操作:插入和查询
\(ch[p][i]\)表示节点\(p\)沿着边\(i\)(\(i\)是字母映射后的值)走到的节点编号
在单词结束点记录插入次数
void insert(char *s)
{
int p=0;
for(int i=0;s[i];i++)
{
int j=s[i]-'a';
if(!ch[p][i]) ch[p][i]=++idx;//如果没有子结点,创建子结点
p=ch[p][j];
}
cnt[p]++;//插入次数
}
int query(char *s)
{
int p=0;
for(int i=0;s[i];i++)
{
int j=s[i]-'a';
if(!ch[p][j]) return 0;
p=ch[p][j];
}
return cnt[p];
}
\(7.\)最大异或对\((01Trie)\)
给定\(N\)个整数\(a_1,a_2,\cdots,a_n\),任选两个数进行异或运算,求得到的结果最大是多少
\(N\)个整数用二进制表示,所以我们的\(Trie\)数位一个二叉树,深度为\(31\)层
const int maxn=100010;
int n,a[maxn];
int ch[maxn*31][2],idx;
void insert(int x)
{
int p=0;
for(int i=30;i>=0;i--)
{
int j=x>>i&1;
if(!ch[p][j]) ch[p][j]=++idx;
p=ch[p][j];
}
}
int query(int x)
{
int p=0,res=0;
for(int i=30;i>=0;i--)
{
int j=x>>i&1;//取出第i位
if(ch[p][!j])//可以找到与之相反的数
{
res+=1<<i;
p=ch[p][!j];
}
else p=ch[p][j];
}
return res;
}
\(8.AC\)自动机
是一个多模式匹配算法:给定\(n\)个模式串和一个主串,查找有多少个模式串在主串中出现过
\(1.\)首先,我们构造\(Trie\)树:用一个节点表示一个从根到当前节点的字符串,例如:节点\(5\)表示字符串\(she\)
如果节点是个模式串,则需打个标记\(cnt[i]=1\)
\(2.\)然后,我们构造\(AC\)自动机:在\(Trie\)上构建两类边:回跳边和转移边
\(ne[v]\)存节点\(v\)的回跳边的终点
回跳边指向父节点的回跳边所指节点的儿子,\((v,u,ne[v],ne[u])\)构成四边形
回跳边所指节点一定是当前节点的最长后缀(如果这里失配,那么我们转移到回跳边去继续匹配)
\(ch[u][i]\)存节点\(u\)的树边的终点,也存节点\(u\)的转移边的终点
转移边指向当前节点的回跳边所指节点的儿子,\((u,ne[u],ch[][])\)构成三角形
转移边所指节点一定是当前节点的最短路(就是说从当前节点到\(i\)的最短路,不必再回到前面的祖先节点去,节约了时间)
void build()//建AC自动机
{
queue<int> q;
for(int i=0;i<26;i++)
if(ch[0][i]) q.push(ch[0][i]);
while(q.size())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=ch[u][i];
if(v) ne[v]=ch[ne[u]][i],q.push(v);
else ch[u][i]=ch[ne[u]][i];//若儿子不存在,则父节点自建转移边
}
}
}
\(3.\)扫描主串匹配
int query(char *s)
{
int ans=0;
for(int k=0,i=0;s[k];k++)
{
i=ch[i][s[k]-'a'];//沿树边或者转移边走,保证不回退
for(int j=i;j&&~cnt[j];j=ne[j])//沿回跳边搜索模式串
{
ans+=cnt[j],cnt[j]=-1;//这样的话,就是只累计不重复的可匹配的字符串个数,如果出现一次就累计一次,就去掉 //cnt=-1和循环里的~cnt的条件
}
}
return ans;
}
模板
https://www.luogu.com.cn/problem/P3808
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#define maxn 1000010
using namespace std;
int ch[maxn][26],cnt[maxn],idx;
int ne[maxn];
char s[maxn];
void insert(char *s)
{
int p=0;
for(int i=0;s[i];i++)
{
int j=s[i]-'a';
if(!ch[p][j]) ch[p][j]=++idx;
p=ch[p][j];
}
cnt[p]++;
}
void build()
{
queue<int> q;
for(int i=0;i<26;i++) if(ch[0][i]) q.push(ch[0][i]);
while(q.size())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=ch[u][i];
if(v) ne[v]=ch[ne[u]][i],q.push(v);
else ch[u][i]=ch[ne[u]][i];
}
}
}
int query(char *s)
{
int ans=0;
for(int k=0,i=0;s[k];k++)
{
i=ch[i][s[k]-'a'];
for(int j=i;j&&~cnt[j];j=ne[j])
{
ans+=cnt[j];
cnt[j]=-1;
}
}
return ans;
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>s;
insert(s);
}
build();
cin>>s;
printf("%d\n",query(s));
return 0;
}
之后有空再学习:
\(9.\)后缀自动机\((SAM)\)
有链接边和转移边
\(ch[x][c]\)存节点\(x\)的转移边的终点,\(fa[x]\)存节点\(x\)的链接边的终点,\(len[x]\)存节点\(x\)的最长串的长度
\(10.\)后缀数组\((SA)\)
\(sa[i]\)表示排名为\(i\)的后缀编号,\(rk[i]\)表示后缀\(i\)的排名,\(height[i]=lap(sa[i],sa[i-1])\)表示第\(i\)名后缀与第\(i-1\)名后缀的最长公共前缀的长度(高度数组表示两个后缀的相似度,排序相邻的两个后缀相似度最高)
void get_sa()//n为后缀个数,m为桶的个数,桶数组x,辅助数组y,计数数组c
{
int i,j,k;
//按第一个字母排序
for(i=1;i<=n;i++) c[x[i]=s[i]]++;//按第一个字母边桶号并累计
for(i=1;i<=m;i++) c[i]+=c[i-1];
for(i=n;i;i--) sa[c[x[i]]--]=i;//后缀i的排序是i所在的桶号的剩余累计值
for(k=1;k<=n;k++)
{
//按第二关键字排序
memset(c,0,sizeof(c);
for(i=1;i<=n;i++) y[i]=sa[i];
for(i=1;i<=n;i++) c[x[y[i]+k]]++;
for(i=1;i<=m;i++) c[i]+=c[i-1];
for(i=n;i;i--) sa[c[x[y[i]+k]]--]=y[i];//后缀y[i]的排序是第二关键字所在的桶号的剩余累计值
}
}
标签:ch,int,char,maxn,字符串,include,节点
From: https://www.cnblogs.com/iwillenter-top1/p/17603399.html