ACM中常用的字符串算法不多,主要有以下几种:
下面来分别介绍一下:
字符串的hash是最简单也最常用的算法,通过某种hash函数将不同的字符串分别对应到不同的数字.进而配合其他数据结构或STL可以做到判重,统计,查询等操作.
一个很简单的hash函数代码如下:
ull xp[maxn],hash[maxn];
void init()
{
xp[0]=1;
for(int i=1;i1]*175;
}
ull get_hash(int i,int L)
{
return hash[i]-hash[i+L]*xp[L];
}
scanf("%s",str);
int n=strlen(str);
hash[n]=0;
for(int i=n-1;i>=0;i--)
{
hash[i]=hash[i+1]*175+(str[i]-'a'+1);
}
其中175是顺便选择的基数,对一个串通过init的预处理后,就用get_hash(i,L)可以得到从位置i开始的,长度为L的子串的hash值.
一般情况下,这个简单的hash函数已经足够好了.但使用hash函数解题的时候还是有问题要注意:
hash函数的结果并不一定准确,hash的值可能会有冲突导致结果错误(但不常遇到可以换hash数即可).
对于一般的字符串,这个hash函数准确性很高. 但是有的题目会刻意构造可以使hash函数失效的字符串,无论换什么样的hash数都过不了,这时就需要对hash函数进行修改,不能使用自然溢出的方式储存hash值,可以选取两个大质数,对用一个字符串记录它的hash值和这两个数的mod.用这种方法可以过掉几乎全部卡hash函数的题
字典树是储存着不同字符串的数据结构,是一个n叉树(n为字符集的大小),对于一棵储存26个字母的字典树来说,它的的每一个节点储存着26个指针可以分别代表这个节点的后面加上’a’~’z’后可以指向那个节点.
插入的时候从根节点开始,沿着对应的边走(如果某个指针后面指向的节点为空.可以新建一个节点),走到字符串结束的时候在当前停留的节点标记一下(是否出现过,出现了几次等).
查询的时候也是一样从根节点走,如果走到某个节点无路可走了,说明查不到.当一路走到字符串结束时,检查当前停留的节点是否被标记过.
一份代码参考:
/*字典树*/
const int CHAR=26,MAXN=100000;
struct Trie
{
int tot,root,child[MAXN][CHAR];
bool flag[MAXN];
Trie()
{
memset(child[1],0,sizeof(child[1]));
flag[1]=true;
root=tot=1;
}
void Insert(const char *str)
{
int *cur=&root;
for(const char*p=str;*p;p++)
{
cur=&child[*cur][*p-'a'];
if(*cur==0)
{
*cur=++tot;
memset(child[tot],0,sizeof(child[tot]));
flag[tot]=false;
}
}
flag[*cur]=true;
}
bool Query(const char *str)
{
int *cur=&root;
for(const char *p=str;*p&&*cur;p++)
cur=&child[*cur][*p-'a'];
return (*cur)&&flag[*cur];
}
}tree;
kmp是一种字符串匹配的算法,普通的字符串匹配需要时间O(n*m) n:字符串长度 m:模版串长度,kmp算法通过对模版串进行预处理来找到每个位置的后缀和第一个字母的前缀的最大公共长度,可以让复制度降低到O(n+m)
关于KMP算法白书有很详细的介绍,网上也有很多.
KMP资料1 , KMP资料2
一种实现:
char t[1000],p[1000];
int f[1000];
void getfail(char* p,int* f)
{
int m=strlen(p);
f[0]=f[1]=0;
for(int i=1;iint j=f[i];
while(j&&p[j]!=p[i]) j=f[j];
f[i+1]=(p[i]==p[j])?j+1:0;
}
}
void kmp(char* t,char* p,int* f)
{
int n=strlen(t),m=strlen(p);
getfail(p,f);
int j=0;
for(int i=0;iwhile(j&&p[j]!=t[i]) j=f[j];
if(p[j]==t[i]) j++;
if(j==m)
{
///i-m+1
/// ans++;
j=f[j];
}
}
}
KMP是单字符串的匹配算法,如果有很多个模版串需要和文本串匹配,就需要用到AC自动机. AC自动机会预处理模版串,插入到一颗字典树中,并处理出fail指针.
具体实现可以看:
AC自动机1 , AC自动机2
我的一个模版:
/*
基于HDOJ 2222 的 AC自动机
文本串对多个模板串的查找
*/
const int maxn=610000;
int ch[maxn][26],fail[maxn],end[maxn];
int root,sz;
char str[1000100];
int newnode()
{
memset(ch[sz],-1,sizeof(ch[sz]));
end[sz++]=0;
return sz-1;
}
void init()
{
sz=0;
root=newnode();
}
void insert(char str[])
{
int len=strlen(str);
int now=root;
for(int i=0;i<len;i++)
{
int& temp=ch[now][str[i]-'a'];
if(temp==-1) temp=newnode();
now=temp;
}
end[now]++;
}
void build()
{
queue<int> q;
fail[root]=root;
for(int i=0;i<26;i++)
{
int& temp=ch[root][i];
if(temp==-1) temp=root;
else
{
fail[temp]=root;
q.push(temp);
}
}
while(!q.empty())
{
int now=q.front(); q.pop();
for(int i=0;i<26;i++)
{
if(ch[now][i]==-1)
ch[now][i]=ch[fail[now]][i];
else
{
fail[ch[now][i]]=ch[fail[now]][i];
q.push(ch[now][i]);
}
}
}
}
int query(char str[])
{
int len=strlen(str);
int now=root;
int ret=0;
for(int i=0;i<len;i++)
{
now=ch[now][str[i]-'a'];
int temp=now;
while(temp!=root&&~end[temp])
{
ret+=end[temp];
end[temp]=-1;
temp=fail[temp];
}
}
return ret;
}
AC自动机+矩阵快速幂也是一种常见的类型:
* BZOJ 1009: [HNOI2008]GT考试
* POJ 2778 DNA Sequence
manacher是处理回文串问题的利器,manancher是一种dp方法和其他字符串关联不大,相对独立,manacher可以在O(1)的时间复杂度内处理出所有的位置的回文串的半径.
一篇很好的介绍: manacher
我的模版
//URAL 1297
//
//
#include
#include
#include
#include
using namespace std;
char str[1100],ans[3300];
int p[3300],pos,how;
void pre()
{
int tot=1;
memset(ans,0,sizeof(ans));
ans[0]='$';
int len=strlen(str);
for(int i=0;i'#';tot++;
ans[tot]=str[i];tot++;
}
ans[tot]='#';
}
void manacher()
{
pos=-1;how=0;
memset(p,0,sizeof(p));
int len=strlen(ans);
int mid=-1,mx=-1;
for(int i=0;iint j=-1;
if(i2*mid-i;
p[i]=min(p[j],mx-i);
}
else p[i]=1;
while(i+p[i]if(p[i]+i>mx)
{
mx=p[i]+i; mid=i;
}
if(p[i]>how)
{
how=p[i]; pos=i;
}
}
}
int main()
{
while(scanf("%s",str)!=EOF)
{
pre();
manacher();
how--;
for(int i=pos-how;i<=pos+how;i++)
{
if(ans[i]!='#') putchar(ans[i]);
}
putchar(10);
}
return 0;
}
manacher在回文串问题中应用还是很多的,回文串自动机也可以处理回文串问题,但是略复杂.
在不用manacher的情况下也可以用 枚举+hash 也可以解决回文串问题. 具体做法可以枚举回文串中心点,二分出这个中心点的最大半径(一个大的半径的回文串肯定包含了小半径的回文串).
这是我曾经出过的一题,用的就是这种想法:
CSU1647: SimplePalindromicTree
后缀数组的主要思想就是将某个字符串的后缀排序,这样取后缀的某一段前缀就是这个字符串的子串.
但是字符串的排序并不是O(1)的,所以后缀数组的代码中主要的一个部分就是为了加字符串的排序快排序速度.
常用的一种排序方法为倍增法
关于后缀数组排序,大白书中有详细的介绍.
罗穗骞后缀数组——处理字符串的有力工具
*神奇的分割线*
以上的方法是非常常见的字符串处理方法,需要很好的理解和运用
下面介绍一些复杂一些的,但是在解决某些问题非常有用的方法
exkmp可以处理出模版串中每个位置i开始和模版开头的最大匹配长度,exkmp可以实现普通kmp的所有功能.
刘雅琼 的《扩展的KMP算法》介绍很好
/*
扩展KMP
next[i]: P[i..m-1] 与 P[0..m-1]的最长公共前缀
ex[i]: T[i..n-1] 与 P[0..m-1]的最长公共前缀
*/
char T[maxn],P[maxn];
int next[maxn],ex[maxn];
void pre_exkmp(char P[])
{
int m=strlen(P);
next[0]=m;
int j=0,k=1;
while(j+11]) j++;
next[1]=j;
for(int i=2;iint p=next[k]+k-1;
int L=next[i-k];
if(i+L1) next[i]=L;
else
{
j=max(0,p-i+1);
while(i+jvoid exkmp(char P[],char T[])
{
int m=strlen(P),n=strlen(T);
pre_exkmp(P);
int j=0,k=0;
while(j0]=j;
for(int i=1;iint p=ex[k]+k-1;
int L=next[i-k];
if(i+L1) ex[i]=L;
else
{
j=max(0,p-i+1);
while(i+j
我的一些扩展kmp的总结
参考上面的连接
后缀自动机的基本思想是:
将一个串的所有后缀加到一颗”字典树”里,由于一个字符串的所有后缀的空间复杂度是O(n^2)的.所以后缀自动机对这棵”字典树”进行了特殊的压缩.
参考资料:
陈立杰营员交流资料
后缀自动机很难理解,要注意掌握几SAM的几个性质.
后缀自动机与线性构造后缀树
SAM的一点性质:
代码中 p->len 变量,它表示该状态能够接受的最长的字符串长度。
该状态能够接受的最短的字符串长度。实际上等于该状态的 fa 指针指向的结点的 len + 1
(p->len)-(p->fa->len):表示该状态能够接受的不同的字符串数,不同的字符串之间是连续的,
既:p 和 p->fa 之间 有最长的公共后缀长度 p->fa->len
num 表示这个状态在字符串中出现了多少次,该状态能够表示的所有字符串均出现过 num 次
序列中第i个状态的子结点必定在它之后,父结点必定在它之前。
既然p出现过,那么p->fa肯定出现过。因此对一个点+1就代表对整条fa链+1.
从root到每一个接收态表示一个后缀,到每一个普通节点表示一个子串
我的实现:
const int CHAR=26,maxn=251000;
struct SAM_Node
{
SAM_Node *fa,*next[CHAR];
int len,id,pos;
SAM_Node(){}
SAM_Node(int _len)
{
fa=0; len=_len;
memset(next,0,sizeof(next));
}
};
SAM_Node SAM_node[maxn*2],*SAM_root,*SAM_last;
int SAM_size;
SAM_Node *newSAM_Node(int len)
{
SAM_node[SAM_size]=SAM_Node(len);
SAM_node[SAM_size].id=SAM_size;
return &SAM_node[SAM_size++];
}
SAM_Node *newSAM_Node(SAM_Node *p)
{
SAM_node[SAM_size]=*p;
SAM_node[SAM_size].id=SAM_size;
return &SAM_node[SAM_size++];
}
void SAM_init()
{
SAM_size=0;
SAM_root=SAM_last=newSAM_Node(0);
SAM_node[0].pos=0;
}
void SAM_add(int x,int len)
{
SAM_Node *p=SAM_last,*np=newSAM_Node(p->len+1);
np->pos=len;SAM_last=np;
for(;p&&!p->next[x];p=p->fa)
p->next[x]=np;
if(!p)
{
np->fa=SAM_root;
return ;
}
SAM_Node *q=p->next[x];
if(q->len==p->len+1)
{
np->fa=q;
return ;
}
SAM_Node *nq=newSAM_Node(q);
nq->len=p->len+1;
q->fa=nq; np->fa=nq;
for(;p&&p->next[x]==q;p=p->fa)
p->next[x]=nq;
}
void SAM_build(char *s)
{
SAM_init();
int len=strlen(s);
for(int i=0;i<len;i++)
SAM_add(s[i]-'a',i+1);
}
/// !!!!!!!!!!!!! 统计每个节点出现的次数
int c[maxn],num[maxn];
SAM_Node* top[maxn];
void Count(char str[],int len)
{
for(int i=0;ilen]++;
for(int i=1;i<=len;i++) c[i]+=c[i-1];
for(int i=0;ilen]]=&SAM_node[i];
SAM_Node *p=SAM_root;
for(;p->len!=len;p=p->next[str[p->len]-'a']) num[p->id]=1; num[p->id]=1;
for(int i=SAM_size-1;i>=0;i--)
{
p=top[i];
if(p->fa)
{
SAM_Node *q=p->fa; num[q->id]+=num[p->id];
}
}
}
去年(2014)新在比赛中出现的数据结构,资料不是很多
用一种类似AC自动机的方法构造出一个字符串的回文串树
Palindromic Tree——回文树【处理一类回文串问题的强力工具】
我的模版:
const int maxn=330000;
const int C=30;
int next[maxn][C];
int fail[maxn];
int cnt[maxn]; // 本质不同的回文串出现的次数(count后)
int num[maxn]; // 表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数
int len[maxn]; // 节点i表示的回文串的长度
int s[maxn]; // 节点i存的字符
int last; // 新加一个字母后所形成的最长回文串表示的节点
int p; // 添加节点的个数 p-2为本质不同的回文串个数
int n; // 添加字符的个数
int newnode(int x)
{
for(int i=0;i0;
cnt[p]=0; num[p]=0; len[p]=x;
return p++;
}
void init()
{
p=0;
newnode(0); newnode(-1);
last=0; n=0;
s[0]=-1; fail[0]=1;
}
int get_fail(int x)
{
while(s[n-len[x]-1]!=s[n]) x=fail[x];
return x;
}
void add(int c)
{
c-='a';
s[++n]=c;
int cur=get_fail(last);
if(!next[cur][c])
{
int now=newnode(len[cur]+2);
fail[now]=next[get_fail(fail[cur])][c];
next[cur][c]=now;
num[now]=num[fail[now]]+1;
}
last=next[cur][c];
cnt[last]++;
}
void count()
{
for(int i=p-1;i>=0;i--) cnt[fail[i]]+=cnt[i];
}
模版代码来自于我的ACM模版:我的ACM模版
大部分例题可以在我的博客:我的博客中找到题解.