字典树

了解它究竟是什么

字典树(trie)是一个神奇的东西.它利用了字符串的公共前缀来存放字符串,从而减小了这些字符串的总空间.
它可以用来解决一些关于前缀的问题.
同理它也能用来存放一些数字之类的.
我们直接根据例题来学习这种数据结构.
Hdu 1251 统计难题
给出n个字符串a[i]并询问k个字符串b[i](输入以回车和EOF结尾),k行,每行输出前缀是b[i]的字符串个数.

/*
这题可以用暴力或者哈希瞎搞.好像都可以过.这里介绍用字典树做的方法.
字典树有一个超级根节点root,这个根节点不存放字母.我写的字典树里rt默认为0.
剩下的每一个节点都存放一个字母,并且包含26个儿子,表示这个节点下面有没有字母.
比如说我们把样例的5个单词加入字典树,形成的结果像这样.我们画图表示,在代码块下面.
实现的代码如下.
*/
const int karen=1e5;//可怜啊,可怜,我真是又爱又恨
//定义结构体trie
struct trie{
//我们需要不断增加节点,因此定义sz初始化为0.
int sz;trie(){sz=0;}
//结点结构体node
struct node{
  int ch[26],cnt;
  //ch是这个节点是否有儿子,以及儿子的编号.cnt是记录该前缀已经被包括的个数,由于本题的要求故增加这一项.
  //由于本题的单词只有小写字母,故开一个26的数组就可以存下所有的儿子节点了.
  node(){cnt=0,memset(ch,0,sizeof cnt);}
  }r[karen|10];//字典树的节点个数可能特别大,需要开到100000.
//插入字符串
void insert(char *s){
  int len=strlen(s),i,p,now=0;//现在now是根节点
  for (i=0;i<len;i++){
    p=s[i]-'a';//我们需要插入这个字符.
    if (!r[now].ch[p]) r[now].ch[p]=++sz;//如果这个节点没有被经历过,我们开一个编号给它.
    now=r[now].ch[p];//这样就到了子节点.
    r[now].cnt++;//表示又多了一个字符串拥有从根节点到这个节点的前缀.
    }
  }
//对前缀询问
int query(char *s){
  int len=strlen(s),i,p,now=0;//一样的操作.
  for (i=0;i<len;++i){
    p=s[i]-'a';
    if (!r[now].ch[p]) return 0;//显然不存在这个子节点就可以返回0了.
    now=r[now].ch[p];
    }
  return r[now].cnt;//最后返回cnt
  }
}my_;
int main()
{
char c[99];
for (;gets(c);)
  {
  re0 len=strlen(c);
  if (!len) break;
  my_.insert(c);
  }
for (;gets(c);) write(my_.search(c)),pl;
}

你这样就成功地A掉了这道题.这题的暴力在800ms上下.
我们再来看一道前缀的问题.
POJ 2001 Shortest Prefixes
给你很多字符串,对于每一个字符串,输出能够确定它与其它字符串不同的前缀.
如果该串太短(即它就是某一个字符串的前缀),输出该串.

/*
思考:建立字典树的方法与上题一样,也需要cnt处理.
建完树后重新按顺序询问字符串,如果到某个节点cnt=1,说明这个前缀已经是独一无二的了,可以直接输出前缀.
如果遍历完了整个字符串也没有答案就输出它本身.
*/
#include<cstdio>
#include<algorithm>
#include<cctype>
#include<cstring>
namespace chtholly{
typedef long long ll;
#define rel register ll
#define rec register char
#define re0 register int
#define gc getchar
#define pc putchar
#define p32 pc(' ')
#define pl puts("")
inline int read(){
  re0 x=0,f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  return x*(f?1:-1);
  }
inline void read(ll &x){
  x=0;re0 f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  x*=f?1:-1;
  }
inline int write(int x){
  if (!x) return pc(48);
  if (x<0) pc('-'),x=-x;
  re0 bit[20],i,p=0;
  for (;x;x/=10) bit[++p]=x%10;
  for (i=p;i;--i) pc(bit[i]+48);
  }
inline char fuhao(){
  rec c=gc();
  for (;isspace(c);c=gc());
  return c;
  }
}
using namespace chtholly;
using namespace std;
const int karen=1e5;
int n;

struct trie{
int sz;trie(){sz=0;}
struct node{
  int ch[26],cnt;
  node(){
    cnt=0;memset(ch,0,sizeof ch);
    } 
  }r[karen];
void insert(char *s){
  int len=strlen(s);
  re0 now=0,p,i;
  for (i=0;i<len;++i){
    p=s[i]-'a';
    if (!r[now].ch[p]) r[now].ch[p]=++sz;
    now=r[now].ch[p];
    r[now].cnt++;
    }
  }
void query(char *s){
  int len=strlen(s);
  re0 now=0,p,i;
  char ans[25]={0};
  for (i=0;i<len;++i){
    p=s[i]-'a';
    ans[i]=p+'a';
    if (r[r[now].ch[p]].cnt==1) return void(puts(ans));
    //为什么这里不是r[now].cnt?对.注意有超级根的存在,第一个节点不是字符串的第一位,而是超级根的某个儿子.当然也可以把下一句放到这句之前.
    now=r[now].ch[p]; 
    }
  puts(s);
  }
}my_;

int main()
{
char c[1010][25];
for (;~scanf("%s",c[++n]);)//读入EOF之后n还会+1.
  my_.insert(c[n]);
for (re0 i=1;i<n;++i) 
  printf("%s ",c[i]),my_.query(c[i]);
}//这样就轻松了.

这样你对字典树的构造以及基本操作都了解了吧.

游戏终于开始了.

Codeforces 948D&&923C Perfect Security
给出两个含有n个数字的数字串A,B,你可以将B中的数字任意排列.
求出A串的每一个数字按照排列与B串中对应数字异或所能得到序列最小的字典序.

这题看似没有任何优化暴力枚举的算法,可是令人震惊的是这题通过字典树转化之后是可以贪心的.
太遗憾了,我在当时比赛的时候不会字典树,否则我就能做出来了.
其实这就是一道01字典树的裸题.

/*
这题可以贪心?你肯定在疑惑,究竟如何贪心?
我们考虑这样的一棵字典树.
它是一棵二叉树,每个节点有0,1两个儿子,从根节点到某叶节点的过程中经过的所有结点从上到下连起来为一个二进制数.
我们把B串的每一个数字插入字典树,按照A串的顺序询问.
这个时候神奇的事情发生了.
我们从高位到低位询问,对于每一个要询问的数,我们让它走和它当前所在位置上的数相同的子节点.
如果不存在的话我们就只能走另一个了.
由于高位的数字大小严格比低位大,所以这样的贪心显然成立.
那么这题就解决了.
你说还有去重?cnt数组记录这个节点已经被询问了几次,如果已经被询问完了这个节点就不给走.
当然直接把询问到的那条路径删掉也是可以的.
可以看到这样做消耗的节点的量是巨大的,数组大小快逼近2e7了.
*/
#include<bits/stdc++.h>
using namespace std;
const int karen=3e5;
int n,a[karen|10];
struct trie{
int sz=0,rt=0;
struct node{
  int ch[2],cnt;
  }r[karen<<5];//9600000的数组
//这里用了递归的建树方法.rt就是刚才的now,d表示现在插进去的是数字的哪一位(从个位开始数)
void insert(int &rt,int x,int d=29){
  if (!rt) rt=++sz;//给一个新的编号
  r[rt].cnt++;//标记走过
  if (~d) insert(r[rt].ch[x>>d&1],x,d-1);//d>=0,我们插入这一位.
  }
int query(int rt,int x,int d=29){
  r[rt].cnt--;
  if (!~d) return 0;
  int p=x>>d&1;//可以发现就是从个位开始数第d位.
  if (r[r[rt].ch[p]].cnt) return query(r[rt].ch[p],x,d-1);//如果可以走就直接贪心,这一位异或值为0.
  else return 1<<d|query(r[rt].ch[!p],x,d-1);//不然只能走相反的,异或值为1,要加上1<<d.
  }
}my_;

int main()
{
int i,x;
scanf("%d",&n);
for (i=1;i<=n;++i) scanf("%d",&a[i]);
for (i=1;i<=n;++i) scanf("%d",&x),my_.insert(my_.rt,x);
for (i=1;i<=n;++i) printf("%d ",my_.query(my_.rt,a[i])); 
}

Thank you for reading the blog!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值