【学习笔记】字符串—广义后缀自动机
一:【前言】
最近一周都在研究 惊(Ren)艳(Lei)无(Zhi)比(Hui)、美(Li)妙(Xing)绝(Yu)伦(Yue) 的自动机,这里引用 \(\text{bztMinamoto}\) 巨佬的一句话来表达此时的心情:
我感觉我整个人都自动机了…… ——\(bztMinamoto\)(回文自动机学习笔记)
在此过程中发现网上讲广义 \(\text{SAM}\) 的文章很少,而且很多都不正确,所以决定整理一下。
二:【引理】
众所周知,\(\text{SAM}\) 的一个经典应用是求一个字符串中本质不同子串数量,那么如果改为求一个 \(\text{Trie}\) 树呢?
刘研绎在 \(2015\) 的国家队论文中说过这样一句话:
大部分可以用后缀自动机处理的字符串的问题均可扩展到 \(Trie\) 树上。
我们将这种建立在 \(\text{Trie}\) 树上的 \(\text{SAM}\) 成为广义 \(\text{SAM}\) 。在学习之前,首先要确保对单串 \(\text{SAM}\) 足够熟悉,
其实也可以简单理解为多串 \(\text{SAM}\) 啦QAQ
三:【算法实现】
在用广义 \(\text{SAM}\) 处理多模式串问题时,网上流传着的主流写法有 \(3\) 种:
\((1).\) 用特殊符号将所有模式串连成一个大串放到一个 \(\text{SAM}\) 中,用一些玄学特判来处理。
\((2).\) 每次插入一个模式串之前,都把 \(last\) 设为 \(1\),按照普通 \(\text{SAM}\) 一样插入,即每个字符串都从起点 \(1\) 开始重新构造。
\((3).\) 用所有模式串建出一颗 \(\text{Trie}\) 树,对其进行 \(bfs\) 遍历构建 \(\text{SAM}\),\(insert\) 时 使 \(last\) 为它在 \(\text{Trie}\) 树上的父亲,其余和普通 \(\text{SAM}\) 一样。
第一种实用性不高且复杂度危险,第二种听机房大佬说是盗版,但因为很少出问题且代码简单,所以很多人都用的这种。但根据广义 \(\text{SAM}\) 的定义,只有第三种才是标准写法。
这里有一个疑问:为什么是 \(bfs\) 而不用 \(dfs\) 呢?在某些特定情况下,\(dfs\) 会被卡成 \(O(n^2)\) 而 \(bfs\) 不会,具体见 这里(翻遍全网找到的唯一一篇细讲广义 \(\text{SAM}\) 准确写法的博客)。
\(bfs\) 代码如下:
//Trie.tr[x]: Trie树的状态转移数组
//Trie.fa[x]: Trie树上节点x的父节点
//Trie.c[x]: Trie树上节点x的字符
//pos[x]:Trie上x节点的前缀字符串(路径 根->x 所表示的字符串)在SAM上的对应节点编号
inline void build(){//bfs遍历Trie树构造广义SAM
for(Re i=0;i<C;++i)if(Trie.tr[1][i])Q.push(Trie.tr[1][i]);//插入第一层字符
pos[1]=1;//Tire树上的根1在SAM上的位置为根1
while(!Q.empty()){
Re x=Q.front();Q.pop();
pos[x]=insert(Trie.c[x],pos[Trie.fa[x]]);//注意是pos[Trie->fa[x]]
for(Re i=0;i<C;++i)if(Trie.tr[x][i])Q.push(Trie.tr[x][i]);
}
}
而实际上建立起 \(\text{Trie}\) 树后再构造 \(\text{SAM}\) 是一种离线写法,我们也可以不建 \(\text{Trie}\) 树直接在线构造:
每次插入一个模式串之前,都把 \(last\) 设为 \(1\),\(insert\) 函数在普通 \(\text{SAM}\) 的基础上加入特判(注意前面说的盗版写法用的是不加特判的普通 \(insert\))。
更改后的 \(insert\) 代码如下:
//link[i]: 后缀链接
//trans[i]: 状态转移数组
inline int insert(Re ch,Re last){//将ch[now]接到last后面
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]])return trans[last][ch];
//已经存在需要的节点(特判1)
Re x,y,z=++O,p=last,flag=0;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{//需要拆分x,将len<=maxlen[p]+1的部分复制一个y出来
if(maxlen[p]+1==maxlen[z]/*或者写:p==last*/)flag=1;(特判2)
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<C;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return flag?y:z;//注意返回值
//返回值为:ch[now]插入到SAM中的节点编号,
//如果now不是某个字符串的最后一个字符,
//那么这次返回值将作为下一次插入时的last
}
加入返回值是方便记录 \(last\)。
接下来我们重点研究一下这两个特判的具体含义:
(特判1)
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]])
return trans[last][ch];
(特判2)
if(maxlen[p]+1==maxlen[z]/*或者写:p==last*/)flag=1;
特判 \(1\) 比较好理解,我们想要在 \(last\) 后面插入一个节点 \(z\) 使得 \(maxlen[z]=maxlen[last]+1\),而这个节点已经存在于\(\text{SAM}\) 中了,那么就可以直接返回。
注意:这里返回的这个节点保存了多个模式串的状态,即将多个不同模式串的相同子串信息压缩在了这一个节点内,如果要记录 \(endpos\) 大小的话,需要给每个模式串都单独开一个 \(siz\) 数组依次更新,而不能全部揉成一坨。【例】
特判 \(2\) 的实质是处理 \(trans[last][ch]\neq NULL\) 且 \(maxlen[last]+1\neq maxlen[trans[last][ch]]\) 的情况。
我们先来看看单串 \(\text{SAM}\) 的 \(insert\) 图示(来源于 \(\text{hihocoder}\)):
在从 \(last\) 开始往前跳 \(link\) 时,单串 \(\text{SAM}\) 中必定存在着 \(trans[p][ch]=NULL\) 的一段,但扩展到多串后可能就没有这一段了,即存在 \(trans[last][ch]=x\),可知 \(maxlen[p]+1\neq maxlen[x]\)(如果相同的话,在特判 \(1\) 时就返回了鸭),如下图:
显然,此时没有任何节点指向最初新建的 \(z\) 节点,同时它没有记录任何信息,新加入的信息全部储存在了 \(link[z]=y\) 节点上面(即 \(x\) 拆分出来的复制点),但通常情况下它作为一个空节点不会对答案造成任何影响(为什么是空的呢?其后缀链接会指向 \(trans[last][ch]\) 的复制节点 \(y\),而 \(maxlen[y]=maxlen[last]+1\),所以 \(minlen[z]=maxlen[link[z]=y]+1=maxlen[last]+2\),又有 \(maxlen[z]=maxlen[last]+1\),所以 \(z\) 为空 )。
从另一个角度看,节点 \(y\) 满足 \(trans[last][ch]=y\) 且 \(maxlen[y]=maxlen[last]+1\),这不正是我们想要的吗(同特判 \(1\)),所以可以返回 \(y\)。
其实通常情况下,不加特判 \(2\) 也不会出啥事,无非就是多跳了一次 \(link\),但在统计某些特定的信息时可能会挂 【例】,所以还是建议推荐加上这一句。
疑问:在线和离线有什么不同呢?
在特判 \(1\) 的作用下,在线写法会构造出一颗类 \(\text{Trie}\) 形态的 \(\text{SAM}\),其本质还是在一颗没有具象化的 \(\text{Trie}\) 树上建立了 \(\text{SAM}\)。
四:【广义SAM的复杂度】
设 \(|T|\) 为 \(\text{Trie}\) 树大小,\(|A|\) 为字符集大小(可视为常数),\(G(T)\) 为 \(\text{Trie}\) 树所有叶节点深度之和。
-
状态数(节点数)依旧为线性 \(O(2|T|)\) 。
-
转移函数(边数)上界为 \(O(|T||A|)\) 。
-
离线时间复杂度为 \(O(|T||A|+|T|)\) 。
-
在线时间复杂度为 \(O(|T||A|+G(T))\) 。
上述性质在 \(2015\) 年刘研绎的国家队论文都中有严谨证明,这里不赘述。
五:【例题】
\(update:2020.3.3\) 发现题库里出现了模板题,决定添加两道例题,并对文章细节进行了修改。
(由于代码较多,可能会显得比较冗长,但广义 \(\text{SAM}\) 的写法具有争议,在各种题目中都能见到一些奇怪的做法,所以我还是把代码放出来供大家参考一下)
【例题一】
传送门:【模板】广义后缀自动机(广义 \(\text{SAM}\)) \(\text{[P6139]}\)
求多串本质不同的子串个数,随便选一种方式建好自动机,答案为:\(\sum maxlen[i]-maxlen[link[i]]\) 。
【Code】
【离线】
#include<algorithm>
#include<cstdio>
#include<queue>
#define Re register int
#define LL long long
using namespace std;
const int N=2e6+5,M=1e6+3;
int n,t;char ch[N];
inline void in(Re &x){
int fu=0;x=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
struct Trie{
int O,c[M],fa[M],tr[M][26];
//fa[x]: Trie树上x的父节点
//c[x]: Trie树上x的颜色
Trie(){O=1;}//根初始化为1
inline void insert(char ch[]){
Re p=1;
for(Re i=1;ch[i];++i){
Re a=ch[i]-'a';
if(!tr[p][a])tr[p][a]=++O,fa[O]=p,c[O]=a;
p=tr[p][a];
}
}
}T1;
struct Suffix_Automaton{
int O,pos[N],link[N],maxlen[N],trans[N][26];queue<int>Q;
//pos[x]:Trie上的x节点(路径1->x所表示的字符串)在SAM上的对应节点编号
//link[i]: 后缀链接
//trans[i]: 状态转移数组
Suffix_Automaton(){O=1;}//根初始化为1
inline int insert(Re ch,Re last){//和普通SAM一样
Re x,y,z=++O,p=last;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<26;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return z;
}
inline void build(){//bfs遍历Trie树构造广义SAM
for(Re i=0;i<26;++i)if(T1.tr[1][i])Q.push(T1.tr[1][i]);//插入第一层字符
pos[1]=1;//Tire树上的根1在SAM上的位置为根1
while(!Q.empty()){
Re x=Q.front();Q.pop();
pos[x]=insert(T1.c[x],pos[T1.fa[x]]);//注意是pos[Trie->fa[x]]
for(Re i=0;i<26;++i)if(T1.tr[x][i])Q.push(T1.tr[x][i]);
}
}
inline void sakura(){
LL ans=0;
for(Re i=2;i<=O;++i)ans+=maxlen[i]-maxlen[link[i]];
printf("%lld\n",ans);
}
}SAM;
int main(){
// freopen("123.txt","r",stdin);
in(n);
for(Re i=1;i<=n;++i)scanf("%s",ch+1),T1.insert(ch);
SAM.build(),SAM.sakura();
}
【在线】
#include<algorithm>
#include<cstdio>
#include<queue>
#define Re register int
#define LL long long
using namespace std;
const int N=2e6+5;
int n;char ch[N];
inline void in(Re &x){
int fu=0;x=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
struct Suffix_Automaton{
int O,link[N],maxlen[N],trans[N][26];
//link[i]: 后缀链接
//trans[i]: 状态转移数组
Suffix_Automaton(){O=1;}//根初始化为1
inline int insert(Re ch,Re last){
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]])return trans[last][ch];//类Trie树形态建树,如果已有这个节点就不再构造了
Re x,y,z=++O,p=last,flag=0;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{
if(maxlen[p]+1==maxlen[z]/*或者写:p==last*/)flag=1;
//p仍然为last,即存在trans[last][ch],那么z肯定是一个没有任何用处的空节点
//所以可直接将z作为x的复制点,也可以忽略z返回复制点y,这里采用的是后者
//普通SAM不存在这种情况
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<26;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return flag?y:z;//注意返回值
}
inline void sakura(){
LL ans=0;
for(Re i=2;i<=O;++i)ans+=maxlen[i]-maxlen[link[i]];
printf("%lld\n",ans);
}
}SAM;
int main(){
// freopen("123.txt","r",stdin);
in(n);
for(Re i=1;i<=n;++i){
scanf("%s",ch+1);Re last=1;
for(Re j=1;ch[j];++j)last=SAM.insert(ch[j]-'a',last);
}
SAM.sakura();
}
【例题二】
求两个字符串的相同子串数量。
上面黑体字后面提到的例题。前面也已经说过了,两个串的 \(|endpos|\) 要分开计算,可以开一个二维数组,用 \(siz[x][id]\) 表示节点 \(x\) 在串 \(id\) 上的 \(endpos\) 大小。
则答案为:\(\sum siz[i][0]*siz[i][1]*(maxlen[i]-maxlen[link[i]])\) 。
【Code】
【离线】
求 \(siz\) 貌似没法用离线做法(如果有请私信联系,我也想知道)。
【在线】
#include<algorithm>
#include<cstdio>
#include<queue>
#define Re register int
#define LL long long
using namespace std;
const int N=8e5+5;
char ch[200003];LL ans;
inline void in(Re &x){
int fu=0;x=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
struct Suffix_Automaton{
int O,ru[N],link[N],maxlen[N],siz[N][2],trans[N][26];queue<int>Q;
//siz[x]: |endpos[x]| 即节点x的endpos大小
Suffix_Automaton(){O=1;}
inline int insert(Re ch,Re last,Re id){
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]]){
siz[trans[last][ch]][id]=1;return trans[last][ch];//注意这里也给siz赋值,因为已经被建好的节点来自其他字符串
}
Re x,y,z=++O,p=last,flag=0;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{
if(p==last)flag=1;
y=++O;maxlen[y]=maxlen[p]+1;;
for(Re i=0;i<26;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
siz[flag?y:z][id]=1;//注意这里也要用flag进行判断
return flag?y:z;
}
inline void sakura(){
for(Re i=2;i<=O;++i)++ru[link[i]];
for(Re i=1;i<=O;++i)if(!ru[i])Q.push(i);
while(!Q.empty()){
Re x=Q.front();Q.pop();
siz[link[x]][0]+=siz[x][0];//分开更新
siz[link[x]][1]+=siz[x][1];
if(!(--ru[link[x]]))Q.push(link[x]);
}
for(Re i=2;i<=O;++i)//统计答案
ans+=(LL)siz[i][0]*siz[i][1]*(maxlen[i]-maxlen[link[i]]);
printf("%lld\n",ans);
}
}SAM;
int main(){
// freopen("123.txt","r",stdin);
for(Re i=0;i<2;++i){
scanf("%s",ch+1);Re last=1;
for(Re j=1;ch[j];++j)last=SAM.insert(ch[j]-'a',last,i);
}
SAM.sakura();
}
【例题三】
传送门:诸神眷顾的幻想乡 \(\text{[ZJOI2015] [P3346]}\) \(\text{[Bzoj3926]}\)
给出一颗叶子结点不超过 \(20\) 个节点的无根树,每个节点上都有一个不超过 \(10\) 的数字,求树上本质不同的路径树(两条路径相同定义为 其路径上所有节点上的数字依次相连组成的字符串相同)。
首先有一个很麻烦的地方是路径可以拐弯(即两端点分别在其 \(lca\) 两个不同儿子节点的子树中),而 \(\text{Trie}\) 和各种自动机在“接受”字符串时都是以根为起点从上往下径直走到底(什么?跳 \(Parent\) 树?你跳任你跳,跳完还是直的)
所以要想办法把路径捋直,瞎 \(yy\) 可能不太容易想出来,这里直接抛结论:
- 一颗无根树上任意一条路径必定可以在以某个叶节点为根时,变成一条从上到下的路径(利于广义 \(\text{SAM}\) 的使用)。
注意到题目中说叶节点不超过 \(20\) 个,这意味着什么?
暴力枚举每一个叶节点作为根节点遍历整棵树啊!
将一共 \(cnt_{leaf}\) 颗树中的所有前缀串都抽出来建立广义 \(\text{SAM}\),然后就可以求本质不同的子串了。 其中前缀串即是从根节点(无根树的某个叶子结点)到任意一个节点的路径所构成的字符串(实际上就是将 \(cnt_{leaf}\) 颗 \(\text{Trie}\) 树合在了一起跑广义 \(\text{SAM}\))。
注意数组大小和空间限制。
【Code】
【离线】
(本题 \(\text{Trie}\) 树的构造方法与其他相比较为特别)
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define Re register int
#define LL long long
using namespace std;
const int N=4e6+5,N20=2e6+3,Nn=1e5+3;
int n,m,o,x,y,t,C,du[Nn],co[Nn],head[Nn];LL ans;
struct QAQ{int to,next;}a[Nn<<1];
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void in(Re &x){
int fu=0;x=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
struct Trie{
int O,c[N20],fa[N20],tr[N20][10];
Trie(){O=1;}
inline int insert(Re p,Re ch){//在p后面插入一个ch
if(!tr[p][ch])tr[p][ch]=++O,c[O]=ch,fa[O]=p;
return tr[p][ch];
}
}T1;
struct Suffix_Automaton{
int O,pos[N],link[N],trans[N][10],maxlen[N];queue<int>Q;
Suffix_Automaton(){O=1;}
inline int insert(Re ch,Re last){
Re x,y,z=++O,p=last;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<C;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return z;
}
inline void build(){
for(Re i=0;i<C;++i)if(T1.tr[1][i])Q.push(T1.tr[1][i]);
pos[1]=1;
while(!Q.empty()){
Re x=Q.front();Q.pop();
pos[x]=insert(T1.c[x],pos[T1.fa[x]]);
for(Re i=0;i<C;++i)if(T1.tr[x][i])Q.push(T1.tr[x][i]);
}
}
inline void sakura(){
for(Re i=2;i<=O;++i)ans+=maxlen[i]-maxlen[link[i]];
printf("%lld\n",ans);
}
}SAM;
inline void dfs(Re x,Re fa,Re fap){//遍历构造Trie树
Re xp=T1.insert(fap,co[x]);//记录在Trie树上的位置,方便下次直接使用
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)!=fa)dfs(to,x,xp);
}
int main(){
// freopen("123.txt","r",stdin);
in(n),in(C),m=n-1;
for(Re i=1;i<=n;++i)in(co[i]);
while(m--)in(x),in(y),add(x,y),add(y,x),++du[x],++du[y];
for(Re i=1;i<=n;++i)if(du[i]==1)dfs(i,0,1);//以此把每个叶子节点作为根插入Trie树
SAM.build(),SAM.sakura();
}
【在线】
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define Re register int
#define LL long long
using namespace std;
const int N=4e6+5,N20=2e6+3,Nn=1e5+3;
int n,m,o,x,y,t,C,du[Nn],co[Nn],head[Nn];LL ans;
struct QAQ{int to,next;}a[Nn<<1];
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void in(Re &x){
int fu=0;x=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
struct Suffix_Automaton{
int O,link[N],trans[N][10],maxlen[N];
Suffix_Automaton(){O=1;}
inline int insert(Re ch,Re last){
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]])return trans[last][ch];
Re x,y,z=++O,p=last,flag=0;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{
if(maxlen[p]+1==maxlen[z]/*或者写:p==last*/)flag=1;
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<C;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return flag?y:z;
}
inline void sakura(){
for(Re i=2;i<=O;++i)ans+=maxlen[i]-maxlen[link[i]];
printf("%lld\n",ans);
}
}SAM;
inline void dfs(Re x,Re fa,Re fap){//遍历在线构造SAM
Re xp=SAM.insert(co[x],fap);//记录x在SAM上的位置,方便下次直接使用
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)!=fa)dfs(to,x,xp);
}
int main(){
// freopen("123.txt","r",stdin);
in(n),in(C),m=n-1;
for(Re i=1;i<=n;++i)in(co[i]);
while(m--)in(x),in(y),add(x,y),add(y,x),++du[x],++du[y];
for(Re i=1;i<=n;++i)if(du[i]==1)dfs(i,0,1);//以此把每个叶子节点作为根插入Trie树
SAM.sakura();
}
六:【参考文献】
来源:oschina
链接:https://my.oschina.net/u/4299119/blog/4289342