轉載: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函數進行修改,不能使用自然溢出的方式儲存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)的時間復雜度內處理出所有的位置的回文串的半徑.
一篇很好的介紹: manacher
我的模版
//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 也可以解決回文串問題. 具體做法可以枚舉回文串中心點,二分出這個中心點的最大半徑(一個大的半徑的回文串肯定包含了小半徑的回文串).
這是我曾經出過的一題,用的就是這種想法:
CSU1647: SimplePalindromicTree
例題
- 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自動機的方法構造出一個字符串的回文串樹
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;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的數據)