eee_hoho
2019-04-28 17:32:24
顾名思义,对于一个字符串,将其各个字符建成树,其中包含一定的父子关系(第这个显然是很好理解的。
我们看着个图,原先的Trie树已经有了
这个操作看起来简单,其实用代码实现起来也是很简单的。
int insert(char *ch) //trie[u][x]表示u节点的x字符指针指向的节点
{
int u=0,l=strlen(ch+1);//u为初始指针指向根节点
for (int i=1;i<=l;++i)
{
int x=ch[i]-'A'; //每次取出字符串中的第i位
if (!trie[u][x])trie[u][x]=++tot; //如果Trie中没有x这个节点,那就插进去
u=trie[u][x]; //往下走
}
p[u]=1; //给结尾打标记
return 0;
}
那么查询操作也是同理,从选取字符串的每一位,从树根往下遍历就可以了。
int query(char *ch)
{
int u(0),l=strlen(ch+1);//u位初始指针指向根节点
for (int i=1;i<=l;++i)
{
int x=ch[i]-'A'; //取出
if (!trie[u][x])return 0; //指针指向空,表示匹配不成功,直接退出
u=trie[u][x]; //往下走
}
return p[u]; //返回结尾标记值
}
时间复杂度:单次
习题
Phone List
给出
因为我们在Trie树插入操作中已经对每一串字符串都打上了标记,所以满足答案的关系只有两种,一种是插入的串是之前的串的前缀,那么只要看在插入过程中是不是没有新增节点,另一种是插入的串包括之前的串,也就是之前的串是插入的串的前缀,这个只要看看有没有走到打了标记的点就好了。
完整代码 把清空写炸了害我交了好几次
L语言
给出
考虑对着
完整代码 不在
秘密消息Secret Message
给出
再用一个数组
完整代码 没啥好说的emmmm
Trie树就只能对着字符串一顿胡乱操作?答案肯定不是。有一类题,让你求出一些数中
就拿刚才的题目——最大异或数对来说,考虑把所有的数转成二进制插入Trie树中,那么接下来有一种贪心的策略:因为对于两个二进制数,只有这一位上不同,也就是一个是
这样说太抽象,我们看下面这张图:
我们把感性理解感性理解
而由于题目的数据一般会比较大,我们需要建一个
代码实现起来也很简单
int query(int x)
{
int u(0),an(0); //an记录当前数x与序列中的数的异或最大值
for (int i=30;i>=0;i--) //从高到低位走
{
int ch=(x>>i)&1; //取出
if (trie[u][!ch])u=trie[u][!ch],an+=1<<i;
//如果可以走反路,就走反路,更新an
else u=trie[u][ch]; //走不了就只能往下走了
}
return an;
}
时空复杂度:单次
完整代码 一定要按题目数据来开树的长度!
习题
Nikitosh 和异或
在一个有
设
我们首先要知道一个异或的性质:对于一个序列
于是设
然后这个题就很好做啦。
完整代码 数组开大千万别RE
最长异或路径
给定一棵
题意感性理解好了
先
完整代码 我竟然写炸了快读???
主席树?????这个Trie有什么关系吗?别着急,当然是有关系啦。没关系我也不会去学啊(大雾
主席树,全名可持久化线段树,是一种支持对历史版本修改和访问的数据结构,主要思想就是对其每个版本都建一棵线段树。
然而这样子空间和时间都受不了,而我们想每次建新的线段树过程中,并不是所有元素都不一样,许多元素都是上个版本继承过来的,于是只要把这些碎片拼在一起,就是主席树了。
我们看这个图片,假设黑色的是第一个版本,红色的是第二个版本,也就是新的版本,当新建红色这个树时,只需要继承上个版本的一些元素,再修改并重新拼接元素就可以了,这样时空复杂度都是很优秀的。
板子题——可持久化数组
基本操作就拿这个题来说吧。
存储变量
struct ff
{
int lc,rc,val; //存放左子树,右子树,权值
}s[40000500];
int n,m,a[40000500],rt[40000500],node_cnt; //a表示序列,rt是根,node_cnt是节点数
建树
void build(int &k,int l,int r)
{
k=++node_cnt; //新的一个节点
if (l==r)
{
s[k].val=a[l];
return;
}
int mid=l+r>>1;
build(s[k].lc,l,mid); //更新左子树
build(s[k].rc,mid+1,r); //更新右子树
}
和线段树的建树方式差不了多少,就不详细说了。
修改
int change(int k,int l,int r,int x,int cnt)
{
int root=++node_cnt; //更新也要新建节点
s[root]=s[k]; //继承
if (l==r)
{
s[root].val=cnt; //点修改
return root;
}
int mid=l+r>>1;
if (x<=mid)s[root].lc=change(s[root].lc,l,mid,x,cnt); //更新左子树
else s[root].rc=change(s[root].rc,mid+1,r,x,cnt); //更新右子树
return root;
}
也和线段树是差不多的。
访问
int query(int k,int l,int r,int x)
{
if (l==r)return s[k].val; //点访问
int mid=l+r>>1;
if (x<=mid)return query(s[k].lc,l,mid,x);//如果在左边,就找左子树
else return query(s[k].rc,mid+1,r,x); //如果在右边,就找右子树
}
这就是主席树啦!是不是很简单。
完整代码 我再也不乱用没返回值的函数了,然后再次感谢评论区的大佬:qwaszx(dsq),Mital。
另一道板子题——可持久化线段树1
先对序列排序+离散化,然后在离散化的数组的基础下建一颗空的主席树,对于每一个区间还不是没理解
完整代码 数组一定不要开小了!
学完主席树,就可以学可持久化Trie啦,我们先看这道题——最大异或和
有一个序列
首先,我们把这个式子拆开,也就变成了似乎很简单,但是这道题还有一个在末尾插入的操作,就需要我们用到可持久化Trie了。
其实可持久化Trie和主席树的思想是类似的,实现方式也有相同之处,就是对每一个前缀异或建一个01Trie,该继承的继承,该修改的修改,这样就完成了。
插入
void insert(int x)
{
int rt=root[node_cnt]; //取出上一个根节点的信息
root[++node_cnt]=++node; //新建节点
for (register int i=24;i>=0;i--)
{
int ch=(x>>i)&1; //取出
size[node]=size[rt]+1; //长度增加
trie[node][ch]=node+1; //给节点编号
trie[node][!ch]=trie[rt][!ch]; //继承上一个根节点的部分子树信息
rt=trie[rt][ch]; //往下走
node++;
}
size[node]=size[rt]+1;
}
访问
void query(int l,int r,int x)
{
int lc=root[l],rc=root[r],ans=0; //取出左右子树
for (register int i=24;i>=0;i--)
{
int ch=(x>>i)&1; //取出
if (size[trie[rc][!ch]]-size[trie[lc][!ch]]>0)
//如果反路有路可走
lc=trie[lc][!ch],rc=trie[rc][!ch],ans|=1<<i;
//走反路并更新答案
else
lc=trie[lc][ch],rc=trie[rc][ch];
//否则只能往下走
}
write(ans);
putchar(10);
}
时空复杂度:总共:
代码应该是比较好理解的
对于这个题,每次插入一次前缀异或,共
完整代码 不会卡常的我写了点奇怪的卡常+
今年省选还考了一道Trie树的题——异或粽子,相信参加省选的dalao看到这么简单的题都切掉了,我这个初四蒟蒻反正看到题是不会做。
这道题其实是让你求在长度为
反正我刚开始是不会做,在dsq神仙的帮助和指点下,彻底的大彻大悟。
其实就是先对序列
那么我们该怎么求呢,在我迷茫无助之时,dsq给我指点了道题序列合并。
拿这道题来说,我们要对序列
回到这道题,仔细一样是不是和那道题的思想一样呢?我们用堆存值,编号(也就是
可是怎么求第
也可以选择去看dsq的题解。
完整代码 一定要开longlong
字符串真是个美妙的东西,我感受到了真正的算法之美,等到5.5回学校巨想对班主任喊一声:“老东西,你教的文化课最没用啦!”