Trie树
L R J LRJ LRJ白书字典树学习总结
我们常常用
T
r
i
e
Trie
Trie[也叫前缀树]来保存字符串集合。
从根节点到每个单词结点的路径上所有字母连接而成的字符串就是该结点对应的字符串。
根节点标号为0,其余结点编号为从1开始的正整数。
具体来说,用
c
h
[
i
]
[
j
]
ch[i][j]
ch[i][j]表示结点
i
i
i的编号为
j
j
j的子结点。这里我们把编号从
0
−
25
0-25
0−25分别对应
26
26
26个字母。
如果需要在单词结点上添加信息,就可以保存在
v
a
l
[
i
]
val[i]
val[i]中。
int ch[maxnode][sigma_size];
int val[maxnode];
struct Trie{
int sz;
Trie(){sz=1;memset(ch[0],0,sizeof(ch[0]));}
int idx(char c){return c-'a';}
void insert(char *s,int v){
int u=0,n=strlen(s);
for(int i=0;i<n;i++){
int c = idx(s[i]);
if(!ch[u][c]){
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;
ch[u][c]=sz++;
}
u=ch[u][c];
}
val[u]=v;//字符串的最后一个字符的附加信息为v,表示是单词结点
}
bool query(char *s){
int u=0,n=strlen(s);
for(int i=0;i<n;i++){
int c=idx(s[i]);
if(!ch[u][c]){
return false;
}
else {
u=ch[u][c];
//val[u]此时可以处理单词节点的前缀信息
}
}
if(!val[u])return false;
else return true;
}
}trie;
还有一种左儿子右兄弟的写法,对应每个点扩展出来的编号,我们采用链表的方式连起来,能够节省很多空间,不过会牺牲一点复杂度。
基本形式一样,但找结点的时候必须
O
(
s
i
g
m
a
−
s
i
z
e
[
字
符
集
大
小
,
26
个
字
母
]
)
O(sigma-size[字符集大小,26个字母])
O(sigma−size[字符集大小,26个字母])
const int maxnode = 2000050;//为总字符串长度*2(原因未知)
int he[maxnode];//左儿子编号
int ne[maxnode];//右兄弟编号
char ch[maxnode];//ch[i]为第i个结点上的字符
int tot[maxnode];//每个节点包含的信息
struct Trie{
int sz;
Trie(){sz=1;tot[0]=he[0]=ne[0];}//初始只有一个0结点
void insert(const char *s,int flag){
int u=0,v,n=strlen(s);
tot[0]++;
for(int i=0;i<n;i++){
bool found=false;//寻找字符,找不到就创建
for(v=he[u];v!=0;v=ne[v]){
if(ch[v]==s[i]){
found=true;
break;
}
}
if(!found){
v=sz++;
tot[v]=0;
ch[v]=s[i];
ne[v]=he[u];
he[u]=v;
he[v]=0;
}
u=v;
tot[u]++;
}
}
}tr;
引入例题:
U
v
a
1401
Uva 1401
Uva1401
题意:给出多个单词,给出一个字符串,求字符串能够由单词拼成的组合数。
容易得到:
d
p
[
i
.
.
.
n
]
=
∑
符
合
前
缀
的
单
词
s
t
r
d
p
[
i
+
l
e
n
(
s
t
r
)
.
.
.
n
]
dp[i...n]=\sum_{符合前缀的单词str}{dp[i+len(str)...n]}
dp[i...n]=∑符合前缀的单词strdp[i+len(str)...n]
只要我们找出
i
.
.
.
n
i...n
i...n的前缀即可。
T
r
i
e
Trie
Trie是个优秀的做法,对于从
i
i
i开始的字符串,只要遇到单词结点我们就执行递推式。
初始化
d
p
[
l
e
n
]
=
1
dp[len]=1
dp[len]=1(就是什么也没有的组合数为1)
_代码弄丢了,有空补
U
v
a
11732
Uva11732
Uva11732
题意:给出
n
n
n个字符串,求出所有字符串互相比较字典序需要的比较次数(比到不同的就不比了)。
相同部门比较次数为
2
l
e
n
2len
2len,不同的为
1
1
1,字符串结尾
"
0
"
"0"
"0"也需要比较。为什么是
2
l
e
n
2len
2len目前未知。
首先一共有
4000
4000
4000个。暴力计算会超时(
4000
∗
4000
∗
1000
4000*4000*1000
4000∗4000∗1000)后面是字符串长度,最大
1000
1000
1000。
我们需要注意的是:我们可以每次记录每个节点经过的单词数,这样每次都可以直接加上这里。
为了考虑不重复加的问题,对于
T
r
i
e
Trie
Trie只有两种情况,完全相同和不同,前者会比较的末尾,所以在末尾我们加上
s
u
m
∗
(
s
u
m
−
1
)
/
2
∗
2
∗
l
e
n
=
s
u
m
∗
(
s
u
m
−
1
)
∗
l
e
n
sum*(sum-1)/2*2*len=sum*(sum-1)*len
sum∗(sum−1)/2∗2∗len=sum∗(sum−1)∗len,对于分叉点
u
u
u的每个孩子节点
p
p
p,
a
n
s
+
=
(
s
u
m
[
u
]
−
s
u
m
[
p
]
)
∗
s
u
m
[
p
]
/
2
∗
(
2
l
e
n
+
1
)
ans+=(sum[u]-sum[p])*sum[p]/2*(2len+1)
ans+=(sum[u]−sum[p])∗sum[p]/2∗(2len+1),显然表示的意思是,对于这个孩子结点,和它不同的比较次数肯定到这一层位置,价值等于前面相同的加上此时不同的
1
1
1,
s
u
m
[
u
]
sum[u]
sum[u]等价于孩子数,除了
s
u
m
[
p
]
sum[p]
sum[p]的其他都和
p
p
p不同字符。
有更简便的做法,但是这里主要是省空间做法的模板。[单词长度过长,空间会炸]
#include<bits/stdc++.h>
#define FOR(i,a,b) for(int i=a;i<=b;i++)
#define ll long long
#define ull unsigned long long
using namespace std;
const int maxnode = 4000 * 1000 + 10;
int he[maxnode];//左儿子编号
int ne[maxnode];//右兄弟编号
char ch[maxnode];//ch[i]为第i个结点上的字符
int tot[maxnode];//每个节点包含的叶节点总数(单词数)
struct Trie{
int sz;
long long ans;//答案
Trie(){sz=1;ans=0;tot[0]=he[0]=ne[0];}//初始只有一个0结点
void insert(const char *s){
int u=0,v,n=strlen(s);
tot[0]++;
for(int i=0;i<=n;i++){
bool found=false;//寻找字符,找不到就创建
for(v=he[u];v!=0;v=ne[v]){
if(ch[v]==s[i]){
found=true;
break;
}
}
if(!found){
v=sz++;
tot[v]=0;
ch[v]=s[i];
ne[v]=he[u];
he[u]=v;
he[v]=0;
}
ans+=(tot[u]-tot[v]-1)*(2*i+1);//cout<<ans<<endl;
if(i==n){
ans+=(tot[v])*(2*i+2);
}
u=v;
tot[u]++;
}
}
}tr;
int main(){
int n,kase=0;
char str[24050];
while(scanf("%d",&n)){
if(n==0)break;
tr=Trie();
FOR(i,1,n){
scanf("%s",str);
tr.insert(str);
}
printf("Case %d: %lld\n",++kase,tr.ans);
}
}
T i r e Tire Tire树在求异或上有很优异的做法,看这里