ACM中字串題常用演算法
轉自http://blog.csdn.net/ck_boss/article/details/47066727
ACM中常用演算法—-字串
ACM中常用的字串演算法不多,主要有以下幾種:
- Hash
- 字典樹
- KMP
- AC自動機
- manacher
- 字尾陣列
- EX_KMP
- SAM(字尾自動機)
- 迴文串自動機
下面來分別介紹一下:
0. Hash
字串的hash是最簡單也最常用的演算法,通過某種hash函式將不同的字串分別對應到不同的數字.進而配合其他資料結構或STL可以做到判重,統計,查詢等操作.
- #### 字串的hash函式:
一個很簡單的hash函式程式碼如下:
ull xp[maxn],hash[maxn];
void init()
{
xp[0 ]=1;
for(int i=1;i<maxn;i++)
xp[i]=xp[i-1]*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值,可以選取兩個大質數,對用一個字串記錄它的hash值和這兩個數的mod.用這種方法可以過掉幾乎全部卡hash函式的題
例題
- HDOJ 4821 String
- HDOJ 4080 Stammering Aliens
- HDOJ 4622 Reincarnation
- CSU1647: SimplePalindromicTree
1. 字典樹
字典樹是儲存著不同字串的資料結構,是一個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;
例題
- POJ 3630 Phone List
- HDOJ 4622 Reincarnation
- HDOJ 1251 統計難題
2. KMP
kmp是一種字串匹配的演算法,普通的字串匹配需要時間O(n*m) n:字串長度 m:模版串長度,kmp演算法通過對模版串進行預處理來找到每個位置的字尾和第一個字母的字首的最大公共長度,可以讓複製度降低到O(n+m)
關於KMP演算法白書有很詳細的介紹,網上也有很多.
一種實現:
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;i<m;i++)
{
int 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;i<n;i++)
{
while(j&&p[j]!=t[i]) j=f[j];
if(p[j]==t[i]) j++;
if(j==m)
{
///i-m+1
/// ans++;
j=f[j];
}
}
}
例題
- HDOJ 1686 Oulipo
- Codeforces 346 B. Lucky Common Subsequence
- KMP+DP: Codeforces 494B. Obsessive String
- ZOJ 3587 Marlon’s String
kmp的應用不一定只在字串中,只要是匹配問題都可以: - CSU 1581 Clock Pictures
3. AC自動機
KMP是單字串的匹配演算法,如果有很多個模版串需要和文字串匹配,就需要用到AC自動機. AC自動機會預處理模版串,插入到一顆字典樹中,並處理出fail指標.
我的一個模版:
/*
基於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;
}
例題
- HDOJ 2222 Keywords Search
- UVA - 11468 Substring
- UvaLA 4670 Dominating Patterns
- HDOJ 2243 考研路茫茫
- POJ 1625 Censored!
- HDOJ 2896 病毒侵襲
- HDOJ 3065 病毒侵襲持續中
AC自動機+矩陣快速冪也是一種常見的型別:
* BZOJ 1009: [HNOI2008]GT考試
* POJ 2778 DNA Sequence
4. manacher
manacher是處理迴文串問題的利器,manancher是一種dp方法和其他字串關聯不大,相對獨立,manacher可以在O(1)的時間複雜度內處理出所有的位置的迴文串的半徑.
我的模版
//URAL 1297
//
//
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
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<len;i++)
{
ans[tot]='#';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;i<len;i++)
{
int j=-1;
if(i<mx)
{
j=2*mid-i;
p[i]=min(p[j],mx-i);
}
else p[i]=1;
while(i+p[i]<len&&ans[i+p[i]]==ans[i-p[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 也可以解決迴文串問題. 具體做法可以列舉迴文串中心點,二分出這個中心點的最大半徑(一個大的半徑的迴文串肯定包含了小半徑的迴文串).
例題
- HDOJ 3613 Best Reward
- URAL 1297 Palindrome
- USACO Calf Flac
5. 字尾陣列
字尾陣列的主要思想就是將某個字串的字尾排序,這樣取字尾的某一段字首就是這個字串的子串.
但是字串的排序並不是O(1)的,所以後綴陣列的程式碼中主要的一個部分就是為了加字串的排序快排序速度.
常用的一種排序方法為倍增法
關於字尾陣列排序,大白書中有詳細的介紹.
例題
- HDOJ 3948 The Number of Palindromes
- HDOJ 4691 Front compression
- POJ 3693 Maximum repetition substring
- POJ 2046 Power Strings
- URAL 1517 Freedom of Choice
- HDOJ 5008 Boring String Problem
- SPOJ 694 Distinct Substrings
- POJ 2774 Long Long Message
- HDOJ 4416 Good Article Good sentence
- HDOJ 4080 Stammering Aliens
*神奇的分割線*
以上的方法是非常常見的字串處理方法,需要很好的理解和運用
下面介紹一些複雜一些的,但是在解決某些問題非常有用的方法
6. EXKMP
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+1<m&&P[j]==P[j+1]) j++;
next[1]=j;
for(int i=2;i<m;i++)
{
int p=next[k]+k-1;
int L=next[i-k];
if(i+L<p+1) next[i]=L;
else
{
j=max(0,p-i+1);
while(i+j<m&&P[i+j]==P[j]) j++;
next[i]=j; k=i;
}
}
}
void exkmp(char P[],char T[])
{
int m=strlen(P),n=strlen(T);
pre_exkmp(P);
int j=0,k=0;
while(j<n&&j<m&&P[j]==T[j]) j++;
ex[0]=j;
for(int i=1;i<n;i++)
{
int p=ex[k]+k-1;
int L=next[i-k];
if(i+L<p+1) ex[i]=L;
else
{
j=max(0,p-i+1);
while(i+j<n&&j<m&&T[i+j]==P[j]) j++;
ex[i]=j; k=i;
}
}
}
例題
- HDOJ 4333 Revolving Digits
- HDOJ 4300 Clairewd’s message
- HDOJ 4763 Theme Section
- UOJ #5. 【NOI2014】動物園
- Codeforces 432 D. Prefixes and Suffixes
- Codeforces 149 E. Martian Strings
7. SAM字尾自動機
字尾自動機的基本思想是:
將一個串的所有後綴加到一顆”字典樹”裡,由於一個字串的所有後綴的空間複雜度是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;i<SAM_size;i++) c[SAM_node[i].len]++;
for(int i=1;i<=len;i++) c[i]+=c[i-1];
for(int i=0;i<SAM_size;i++) top[--c[SAM_node[i].len]]=&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];
}
}
}
例題
- Codeforces 235C. Cyclical Quest
- HDOJ 4416 Good Article Good sentence
- SPOJ 1811. Longest Common Substring LCS
- SPOJ 8222 NSUBSTR Substrings
- HDOJ 3518 Boring counting
- SPOJ LCS2 1812. Longest Common Substring II
7. 迴文串自動機
去年(2014)新在比賽中出現的資料結構,資料不是很多
用一種類似AC自動機的方法構造出一個字串的迴文串樹
我的模版:
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;i<C;i++) next[p][i]=0;
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];
}
例題
- BZOJ 3676 Apio2014 迴文串
- 2014 Xi’an Regional G The Problem to Slow Down You
(迴文串自動機+hash有卡自然溢位hash的資料)