字典树
Posted iwillenter-top1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字典树相关的知识,希望对你有一定的参考价值。
(并不是按照难度来排序的)
(1.doubt)
题意:
(VKorpela)很喜欢异或,有一天,他看到(Serene)写下了两个长度都为(n)的数组(a)和(b),他想对(a)和(b)分别按照某种方式排序,然后构造一个数组(c),满足(??_?? = ??_?? xor ??_??) 。他想请你告诉他字典序最小的(c)
分析:
一看就知道是字典树对不对,但是不会打(Σ( ° △ °|||)︴)
题解思路如下:对于(a)和(b)分别建一棵字典树,两棵树一起(dfs)使得(a_i)与(b_j)抵消。
由异或的性质:如果两者相同,就可以相消,所以先合并(lson[A],lson[B]),再合并(rson[A])和(rson[B])(这两步的顺序可以调换),再合并(lson[A],rson[B],)最后合并(rson[A],lson[B])(同样的,这两步的顺序也可以调换,为什么可以调换:因为(lson)与(rson)之间互不影响).如果看不懂,就请看代码:
(Code:)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxm=12e6+10;
const int maxn=2e5+10;
int son[maxm][2],sum[maxm],tot;
int a[maxn],b[maxn],c[maxn];
int cnt;
void insert(int x,int rt)//因为是建两棵树
{
++sum[rt];//这不应该是没有必要的,去掉之后依然A掉
for(int i=(1<<30);i;i>>=1)//2^0=1
{
bool v=x&i;
if(!son[rt][v]) son[rt][v]=++tot;
rt=son[rt][v];
++sum[rt];
}
}
#define l1 sum[son[p1][0]]
#define r1 sum[son[p1][1]]
#define l2 sum[son[p2][0]]
#define r2 sum[son[p2][1]]
int dfs(int p1,int p2,int x,int d)
{
int u=0;
if(d<0)//加上两棵树的总共有32层,所以最底层的时候d=-1
{
u=min(sum[p1],sum[p2]);
sum[p1]-=u,sum[p2]-=u;
for(int i=1;i<=u;++i) c[++cnt]=x;
return u;
}
if(l1&&l2) u+=dfs(son[p1][0],son[p2][0],x,d-1);//d-1
if(r1&&r2) u+=dfs(son[p1][1],son[p2][1],x,d-1);
if(l1&&r2) u+=dfs(son[p1][0],son[p2][1],x+(1<<d),d-1);//因为在这一位的时候两者不同异或后的结果要加上(1<<d)(表示的是它儿子这一位要加上1<<d)
if(r1&&l2) u+=dfs(son[p1][1],son[p2][0],x+(1<<d),d-1);//原因同上
sum[p1]-=u,sum[p2]-=u;
return u;
}
void clear()
{
for(int i=1;i<=tot;++i) son[i][0]=son[i][1]=0,sum[i]=0;
tot=2,cnt=0;//因为两棵树的根节点分别记为了1和2
}
int main()
{
freopen("doubt.in","r",stdin);
freopen("doubt.out","w",stdout);
int n;
scanf("%d",&n);
clear();
for(int i=1;i<=n;++i) scanf("%d",&a[i]),insert(a[i]);
for(int i=1;i<=n;++i) scanf("%d",&b[i]),insert(b[i]);
dfs(1,2,0,30);
sort(c+1,c+n+1);//要使字典序最小,一定要先排序
for(int i=1;i<n;++i) printf("%d ",c[i]);
printf("%d
",c[n]);
return 0;
}
(2.Luogu2922) (Secret Message)
题意:
贝茜正在领导奶牛们逃跑.为了联络,奶牛们互相发送秘密信息.
信息是二进制的,共有(M(1≤M≤50000))条.反间谍能力很强的约翰已经部分拦截了这些信息,知道了第(i)条二进制信息的前(b_i(l<b_i≤10000))位.他同时知道,奶牛使用(N(1≤N≤50000))条密码.但是,他仅仅了解第(j)条密码的前(c_j(1≤c_j≤10000))位.
对于每条密码(j),他想知道有多少截得的信息能够和它匹配.也就是说,有多少信息和这条密码有着相同的前缀.当然,这个前缀长度必须等于密码和那条信息长度的较小者.
在输入文件中,位的总数(即(∑B_i+∑C_i))不会超过(500000).
分析:
很简单的模板题
对于密码,若它与信息有相同的前缀(题目中的前缀有定义,并非我们平时所说的前缀),有两种情况:
(1.)密码包含信息
(2.)信息包含密码
第一种情况边走边统计即可,第二种情况,在最末的时候加上经过这个节点的字符串个数,同时又要减去在这个节点结束的字符串个数,因为在之前已经加上了.
(Code:)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=500010;
const int maxm=5e7+10;
int son[maxm][2],sum[maxm];
int a[maxn];
int add[maxn];
int tot=0;
void insert(int *s,int num)
{
int rt=0;
for(int i=1;i<=num;++i)
{
int c=s[i];
if(!son[rt][c]) son[rt][c]=++tot;
rt=son[rt][c];
++add[rt];
}
++sum[rt];
}
int query(int* s,int num)
{
int rt=0;
int res=0;
for(int i=1;i<=num;++i)
{
int c=s[i];
if(!son[rt][c]) return res;
else
{
rt=son[rt][c];
res+=sum[rt];
}
}
return res+add[rt]-sum[rt];
}
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=m;++i)
{
int k;
scanf("%d",&k);
for(int j=1;j<=k;++j)
{
scanf("%d",&a[j]);
}
insert(a,k);
}
for(int i=1;i<=n;++i)
{
int k;
scanf("%d",&k);
for(int j=1;j<=k;++j)
{
scanf("%d",&a[j]);
}
printf("%d
",query(a,k));
}
return 0;
}
(3.)单词数
注:因为我不用(hdu),所以下面的代码也没有经过评测
题意:
统计一篇文章中不同的单词数
分析:
也是模板题(其实如果只考字典树的话,都只是在它的模板上稍微修改一下即可,只有第一题可能比较难),收获也就是(stringstream)的应用
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=2e6+10;
int ch[maxn][30];
int tot=0;
int ans=0;
bool flag[maxn];
void insert(char* a)
{
int rt=0;
int l=strlen(a);
for(int i=0;i<l;++i)
{
int c=a[i]-'a';
if(!ch[rt][c]) ch[rt][c]=++tot;
rt=ch[rt][c];
}
if(!flag[rt]) ++ans;
flag[rt]=true;
}
void clear()
{
for(int i=0;i<=tot;++i)
{
flag[i]=false;
for(int j=0;j<=26;++j)
{
ch[i][j]=0;
}
}
tot=ans=0;
}
int main()
{
std::ios::sync_with_stdio(0)
string s,tp;
while(getline(cin,s))
{
if(s==="#") break;
stringstream ss(s);
while(ss>>tp)
{
insert(tp);
}
printf("%d
",ans);
clear();
}
return 0;
}
有一个更简易的(set)做法,相信大家都会,我就不写啦(QAQ)
(4.POJ) (1816) (Wild Words)
题意:
给出(n)个模式串,串中除开小写字母外,(?)代表一个字符,(*)代表可空的任意字符串,然后再给出(m)个字符串,问有多少个模式串可以与之匹配。
分析:
看有一个题解写的是这个东西叫做字符串模糊匹配,其实好像就是一个(tire+dfs)
总的来说,除了('?')和('#')的处理外,这就是一个字典树的模板题
对于('?')的处理,就把它的编号设为(26),任意一个字符经过它都可以继续往下走。
对于('#')的处理,每次让它匹配(1)个字符,(2)个字符(,3)个字符(,...,)只要使它的长度始终小于(len)即可,详情请见代码:
(Code:)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=6e5+10;
int ch[maxn][30];
bool flag[maxn];
int tot=0;
int en[100000];
bool ans[maxn];
int n,m;
int ID(char s)
{
if(s=='?') return 26;
if(s=='*') return 27;
return s-'a';
}
int insert(char* s)
{
int rt=0;
int len=strlen(s);
for(int i=0;i<len;++i)
{
int c=ID(s[i]);
if(!ch[rt][c]) ch[rt][c]=++tot;
rt=ch[rt][c];
}
flag[rt]=true;
return rt;
}
void check(char* s,int pos,int c)
{
if(pos==strlen(s)&&flag[c])
{
ans[c]=true;
}
int num=s[pos]-'a';
if(num>=0&&num<=25&&ch[c][num]) check(s,pos+1,ch[c][num]);
if(ch[c][26]) check(s,pos+1,ch[c][26]);
if(ch[c][27])
{
int exc=pos;
while(exc<=strlen(s))
{
check(s,exc,ch[c][27]);
++exc;
}
}
}
int main()
{
char s[30];
scanf("%d%d",&n,&m);
for(int i=0;i<n;++i)
{
cin>>s;
en[i]=insert(s);
}
for(int i=0;i<m;++i)
{
cin>>s;
memset(ans,false,sizeof(ans));
int match=0;
check(s,0,0);
for(int j=0;j<n;++j)
{
if(ans[en[j]]) printf("%d ",j),match++;
}
if(!match) printf("Not match");
putchar('
');
}
return 0;
}
以上是关于字典树的主要内容,如果未能解决你的问题,请参考以下文章