字典樹(trie樹)
引入
當我們走進圖書館的閱覽室尋找書時,會不由自主地根據書架上的分類標籤尋找自己所喜好的書籍;當打開電腦中的資源管理器時,我們會看到一層一層的目錄結構。它們的存在,方便了我們生活中的一個重要的問題——檢索。
在信息學競賽( Olympiad in Informatics ,簡稱 OI)的學習過程中,我們也經常會遇到關於“檢索”的問題。而通常採用的不借助任何數據結構(Data Structure)的的枚舉方法,雖然簡單易寫,但往往存在着效率低下的弊端。那我們如何才能通過簡單的途徑提高算法中的檢索效率?
百科名片
在計算機科學中,trie,又稱前綴樹或字典樹,是一種有序樹,用於保存關聯數組,其中的鍵通常是字符串。與二叉查找樹不同,鍵不是直接保存在節點中,而是由節點在樹中的位置決定。一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串。
例1 單詞查找樹
NOI-2000 Luogu-5755 CodeVS-1729
鏈接:
Luogu: https://www.luogu.com.cn/problem/P5755
CodeVS: http://codevs.cn/problem/1729/
CodeVS的鏈接給了好像沒啥用
題目描述
在進行文法分析的時候,通常需要檢測一個單詞是否在我們的單詞列表裏。爲了提高查找和定位的速度,通常都要畫出與單詞列表所對應的單詞查找樹,其特點如下:
- 根節點不包含字母,除根節點外每一個節點都僅包含一個大寫英文字母;
- 從根節點到某一節點,路徑上經過的字母依次連起來所構成的字母序列,稱爲該節點對應的單詞。單詞列表中的每個詞,都是該單詞查找樹某個節點所對應的單詞;
- 在滿足上述條件下,該單詞查找樹的節點數最少。
對一個確定的單詞列表,請統計對應的單詞查找樹的節點數(包括根節點)。
輸入描述
一個單詞列表,每一行僅包含一個單詞。每個單詞僅由大寫的英文字符組成,長度不超過 6363 個字符。文件總長度不超過 32K,至少有一行數據。
輸出描述
僅包含一個整數。該整數爲單詞列表對應的單詞查找樹的節點數。
樣例輸入
A
AN
ASP
AS
ASC
ASCII
BAS
BASIC
樣例輸出
13
思路
字母樹的插入(Insert)、刪除( Delete)和查找(Find)都非常簡單,用一個一重循環即可,即第 i次循環找到前 i個字母所對應的子樹,然後進行相應的操作。實現這棵字母樹,我們用最常見的數組保存即可,當然也可以開動態的指針類型。至於結點對兒子的指向,一般有三種方法:
1 對每個結點開一個字母集大小的數組,對應的下標是兒子所表示的字母,內容則是這個兒子對應在大數組上的位置,即標號;
2 對每個結點掛一個鏈表,按一定順序記錄每個兒子是誰;
3 使用左兒子右兄弟表示法記錄這棵樹。
三種方法,各有千秋。第一種易實現,但實際的空間要求較大;第二種,較易實現,空間要求相對較小,但比較費時;第三種,空間要求最小,但相對費時且不易寫。但總的來說,幾種實現方式都是比較簡單的,只要在做題時加以合理選擇即可,本文不再贅述。
代碼
和“思路”配套的代碼:
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=1e5+5;
struct trie {
int ch[26];
} tr[N];
int tot=0;
char s[110];
inline void cl(int k) {
for(int i=0; i<26; i++) tr[k].ch[i]=0;
}
void insert() {
int x=0,len=strlen(s);
for(int i=0; s[i]; i++) {
if(tr[x].ch[s[i]-'A']==0) {
tr[x].ch[s[i]-'A']=++tot;
cl(tot);
}
x=tr[x].ch[s[i]-'A'];
}
}
int main(){
while(scanf("%s",s)!=EOF) insert();
printf("%d\n",tot+1);
return 0;
}
更簡短的代碼:
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=1e5+5;
int tot;
int t[N][26];
char s[65];
void insert(char *s) {
int k=0;
for(int i=0; s[i]; i++) {
int x=s[i]-65;
k=t[k][x] ? t[k][x]: t[k][x]=++tot;
}
}
int main(){
while(scanf("%s",s)!=EOF) insert(s);
printf("%d\n",tot+1);
return 0;
}
正式開始介紹字典樹
字典樹,又稱單詞查找樹、Trie樹,是 一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來節約存儲空間,最大限度地減少無謂的字符串比較,查詢效率比哈希表高。
下面描述建樹過程:
單詞 | 分數 |
---|---|
a | 2 |
ab | 4 |
abc | 5 |
abd | 9 |
bcd | 5 |
bc | 3 |
cde | 7 |
de | 5 |
d | 2 |
e | 2 |
‘
建樹完成。
trie樹的指針寫法
struct dictree {
dictree *child[26];
int n; //根據需要變化
};
dictree *root;
例2 統計難題
HDU-1251
鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1251
https://vjudge.net/problem/HDU-1251
題目描述
Ignatius最近遇到一個難題,老師交給他很多單詞(只有小寫字母組成,不會有重複的單詞出現),現在老師要他統計出以某個字符串爲前綴的單詞數量(單詞本身也是自己的前綴).
輸入描述
輸入數據的第一部分是一張單詞表,每行一個單詞,單詞的長度不超過10,它們代表的是老師交給Ignatius統計的單詞,一個空行代表單詞表的結束.第二部分是一連串的提問,每行一個提問,每個提問都是一個字符串.
注意:本題只有一組測試數據,處理到文件結束.
輸出描述
對於每個提問,給出以該字符串爲前綴的單詞的數量.
樣例輸入
banana
band
bee
absolute
acm
ba
b
band
abc
樣例輸出
2
3
1
0
代碼
注意:本題提交時選擇的語言應爲C++,不要選G++。選G++會內存超限的(不清楚原因)。
正常的寫法:
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=1000005;
int tot=1;
int n[N];
int t[N][26];
char s[150];
void insert(char *s) {
int k=0;
for(int i=0; s[i]; i++) {
int x=s[i]-'a';
if(!t[k][x]) t[k][x]=tot++;
k=t[k][x];
n[k]++;
}
}
int find(char *s) {
int p=0;
for(int i=0; s[i]; i++) {
int x=s[i]-'a';
if(t[p][x]) p=t[p][x];
else return 0;
}
return n[p];
}
int main() {
while(1) {
gets(s);
if(!strcmp(s,"")) break;
insert(s);
}
while(scanf("%s",s)!=EOF) cout << find(s) << endl;
return 0;
}
指針的寫法:
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=1000005;
struct dictree {
struct dictree *child[26];
int n;
};
struct dictree *root;
void insert(char *source) {
int len,i,j;
struct dictree *current,*newnode;
len=strlen(source);
if(len==0) return;
current=root;
for(i=0; i<len; i++) {
if(current->child[source[i]-'a']!=0) {
current=current->child[source[i]-'a'];
current->n=current->n+1;
} else {
newnode=(struct dictree *)malloc(sizeof(struct dictree));
for(j=0; j<26; j++) newnode->child[j]=0;
current->child[source[i]-'a']=newnode;
current=newnode;
current->n=1;
}
}
}
int find(char *source) {
int i,len;
struct dictree *current;
len=strlen(source);
if(len==0) return 0;
current=root;
for(i=0;i<len;i++) {
if(current->child[source[i]-'a']!=0) current=current->child[source[i]-'a'];
else return 0;
}
return current->n;
}
int main() {
char temp[11];
int i,j;
root=(struct dictree *)malloc(sizeof(struct dictree));
for(i=0; i<26; i++) root->child[i]=0;
root->n=2;
while(gets(temp),strcmp(temp,"")!=0) insert(temp);
while(scanf("%s",temp)!=EOF) {
i=find(temp);
printf("%d\n",i);
}
}
例3 Remember the Word
LA-3942
鏈接:https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=1943
https://vjudge.net/problem/UVALive-3942
題目大意
給定一個長度不超過300000的字符串str,然後給定n(n<=4000)個長度不超過100的字符串ai,問用ai組合成str有多少種方案數,最終結果mod 20071027。
思路
分析dp[i]表示(i到n)的串有幾種表示方法。dp[i]=sigma(dp[j]) j>i 並且s[i…j-1]組成單詞如果枚舉j,判斷是否組成單詞,複雜度非常高。
我們可以把所有的單詞組成trie樹,然後只要在沿着trie樹上去匹配就可以,最多找
100次(每個單詞的最大長度是100)。
代碼
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=300005;
int const MOD=20071027;
int len,n,tot;
int f[N],ed[N];
int tr[N][26];
char s[N],ts[1005];
void build(char *s) {
int k=0;
int len=strlen(s);
for(int i=0; i<len; i++) {
int id=s[i]-'a';
if(tr[k][id]==0) tr[k][id]=++tot;
k=tr[k][id];
}
ed[k]++;
}
void find(int x) {
int k=0;
for(int i=x; i<=len; i++) {
int id=s[i]-'a';
if(tr[k][id]==0) break;
k=tr[k][id];
if(ed[k]>0) f[x]=(f[x]+f[i+1])%MOD;
}
}
int main() {
int cas=0;
while(scanf("%s",s+1)!=EOF) {
len=0;
for(int i=1; s[i]; i++,len++);
scanf("%d",&n);
memset(tr,0,sizeof(tr));
memset(ed,0,sizeof(ed));
tot=0;
for(int i=0; i<n; i++) {
scanf("%d",ts);
build(ts);
}
memset(f,0,sizeof(f));
f[len+1]=1;
for(int i=len; i>=1; i--) {
scanf("%s",ts);
find(i);
}
printf("Case %d: %d\n",++cas,f[1]);
}
return 0;
}
例4 “strcmp()” Anyone?
UVA-11732
鏈接:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2832
https://vjudge.net/problem/UVA-11732
題目大意
int strcmp(char *s, char *t)
{
int i;
for (i=0; s[i]==t[i]; i++)
if (s[i]=='\0')
return 0;
return s[i] - t[i];
}
如上所述,比較操作一直進行到兩個字符串的對應位置處的字符不相同位置,比如than和that there 和the 各需要比較7次比較。
輸入n個字符串串,兩兩調用一次strcmp,問總共要比較多次?
n<=4000,字符串長度不超過1000
思路
兩兩比較顯然不現實,我們可以把單詞一次插入到trie樹裏面,邊插入,邊計算。
由於最多可能會有4000*1000個字符,簡單的二維數組表示法無能爲力,只能採用左兒子,右兄弟的表示法。
代碼
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=4000005;
struct Edge {
int to,nt;
} e[N];
int tot,cnt,n;
int h[N],sum[N],ed[N];
ll ans;
char s[1005],z[N];
inline void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
void build(char *s) {
int k=0;
for(int i=0; s[i]; i++) {
int t=-1;
for(int j=h[k]; j!=-1; j=e[j].nt) {
int v=e[j].to;
if(z[v]==s[i]) t=v;
}
if(t==-1) {
add(k,++tot);
t=tot;
z[tot]=s[i];
}
if(k>0) ans=ans+(sum[k]<<1);
ans=ans+sum[k]-sum[t];
sum[k]++;
k=t;
}
ans=ans+3*sum[k]+ed[k];
sum[k]++;
ed[k]++;
}
int main() {
int cas=0;
while(scanf("%d",&n)!=EOF && n>0) {
cnt=tot=ans=0;
memset(h,-1,sizeof(h));
memset(sum,0,sizeof(sum));
memset(ed,0,sizeof(ed));
for(int i=0; i<n; i++) {
scanf("%s",s);
build(s);
}
printf("Case %d: %lld\n",++cas,ans);
}
return 0;
}
例5 最長公共前綴問題(模板題)
串的最長公共前綴(Longest Common Prefix,簡稱LCP)問題。
題目描述
給出N個小寫英文字母串,以及Q個詢問,即詢問某兩個串的最長公共前綴的長度是多少。
輸入描述
第一行,兩個數字N和Q;
接下來N行,每行一個字母串;接下來Q行,每行一個兩個數字,表示對這兩個編號的串進行詢問。
輸出描述
Q行,每行是對應問題的答案。
樣例輸入
2 2
abc
abd
1 1
1 2
樣例輸出
3
2
思路
明顯的,兩個串的最長公共前綴問題可以轉換爲trie樹上的最近公共祖先問題。
1.利用並查集(Disjoint Set)採用經典的Tarjan 算法;
2.對於字母樹上的每個結點,遞推求出其所有向上2k後的祖先。查找兩個點的最近公共祖先就可以通過它們同時快速地向上跳躍儘可能大的距離得到了。
其他練習
-
POJ-1204
鏈接:http://poj.org/problem?id=1204
https://vjudge.net/problem/POJ-1204 -
POJ-2001
鏈接:http://poj.org/problem?id=2001
https://vjudge.net/problem/POJ-2001 -
POJ-3630
鏈接:http://poj.org/problem?id=3630
https://vjudge.net/problem/POJ-3630 -
POJ-3690
鏈接:http://poj.org/problem?id=3690
https://vjudge.net/problem/POJ-3690 -
HDU-2852
鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=2852
https://vjudge.net/problem/HDU-2852