Trie
简要说明
- (Trie) 树在多个串的检索中十分实用,也是自动机的基础.
- 它可以将若干个字符串组合成一颗树,不少题可以建好 (Trie) 树后在上面树形 (dp) .
- 另外一个应用是解决最大异或值的问题,将数看成 (01) 串,贪心地在 (Trie) 树上走.
题目
Phone List
- 题意:给若干数字串,问有没有两个串满足一个串是另一个串的前缀.
- 将读入的串一个个插入 (Trie) 树中,不难发现串 (A) 是串 (B) 的前缀,只有两种情况.
- 一种情况是串 (A) 先被插入,那么在插入串 (B) 时,一定会经过 (A) 的单词节点.
- 另一种情况是串 (B) 先被插入,那么在插入串 (A) 时,都是沿着 (B) 的边走的,并没有新建边.
- 一边插入,一边判断这两种情况是否出现即可.
- 另一个简单而有效的做法是(hash).将出现过的前缀全部用 (vis) 数组标记即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=5e5+10;
const int Siz=15;
int tcnt=0;
struct Trie{
inline int idx(char c)
{
return c-'0';
}
int ch[MAXN][Siz];
int val[MAXN];
int cnt;
void init()
{
memset(ch,0,sizeof ch);
cnt=0;
}
int ins(char *s)
{
int n=strlen(s);
int u=0;
int f=0;
for(int i=0;i
The XOR Largest Pair
- 题意:给若干个整数,求出两两异或能得到的最大值.
- 使用 (Trie) 树,将每个数看做一个长度为 (32) 的字符串插入 (Trie) 树中.
- 插入完毕后,枚举一个数,在 (Trie) 树中往下走,尽量使每步的 (0/1) 和枚举的数都不一样.
- 这样贪心取即可求出答案.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
int idx;
int ch[10*MAXN][2];
Trie()
{
idx=0;
memset(ch,0,sizeof ch);
}
void ins(int x)
{
int u=0;
for(int i=MAXL;i>=0;--i)
{
int k=(x>>i)&1;
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
}
int maxxor(int x)
{
int u=0;
int res=0;
for(int i=MAXL;i>=0;--i)
{
int k=(x>>i)&1;
if(ch[u][k^1])
{
res<<=1;
res+=1;
u=ch[u][k^1];
}
else
{
res<<=1;
u=ch[u][k];
}
}
return res;
}
}T;
int a[MAXN];
int main()
{
int n=read();
for(int i=1;i<=n;++i)
{
a[i]=read();
T.ins(a[i]);
}
int ans=0;
for(int i=1;i<=n;++i)
ans=max(ans,T.maxxor(a[i]));
printf("%d
",ans);
return 0;
}
Nikitosh 和异或
- 题意:给出 (n) 个整数,选出两个严格不相交的区间 (A=[l_1,r_1],B=[l_2,r_2]) ,使得 (A) 的异或和加上 (B) 的异或和最大,求出这个最大值.
- 若记录前缀异或和 (s) ,根据异或的性质,(x xor x =0,)有(a[l_1] xor a[l_1+1] xor ......a[r_1]=s[r_1] xor s[l_1-1].)
- 可以记(l[i])表示在(i)左侧选出区间的最大异或值,(r[i])表示在(i)右侧选出区间的最大异或值.
- 将前/后缀依次插入 (Trie) 中,找出最大异或和即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
int idx;
int ch[10*MAXN][2];
void ins(int x)
{
int u=0;
for(int i=MAXL;i>=0;--i)
{
int k=(x>>i)&1;
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
}
int maxxor(int x)
{
int u=0;
int res=0;
for(int i=MAXL;i>=0;--i)
{
int k=(x>>i)&1;
if(ch[u][k^1])
{
res<<=1;
res+=1;
u=ch[u][k^1];
}
else
{
res<<=1;
u=ch[u][k];
}
}
return res;
}
void init()
{
idx=0;
memset(ch,0,sizeof ch);
ins(0);//避免1个数求不出的情况
}
Trie()
{
init();
}
}T;
int a[MAXN],n;
int l[MAXN],r[MAXN];//i两侧的最大值
int main()
{
n=read();
int cur=0;
for(int i=1;i<=n;++i)
{
a[i]=read();
cur^=a[i];
T.ins(cur);
l[i]=max(l[i-1],T.maxxor(cur));
}
cur=0;
T.init();
for(int i=n;i>=1;--i)
{
cur^=a[i];
T.ins(cur);
r[i]=max(r[i+1],T.maxxor(cur));
}
int ans=0;
for(int i=1;i
L 语言
- 题意:给出若干个单词和若干篇文章.对于每篇文章,求出它能被完全匹配的最大前缀长度.
- 将单词全部插入 (Trie) 树中.用(f[i])表示前缀(1) 到 (i) 能否被单词完全匹配.
- 匹配文章时,应先连好失配边(所以这其实是一个 (AC) 自动机的题对吧).
- 若当前匹配到了单词结点,且这个单词的开头的前面都能被完全匹配,显然这个位置的前缀可以被完全匹配,更新答案.
- 在插入单词的时候将权值 (val) 设置为单词长度记录下来即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=1e6+10;
const int Siz=26;
char buf[MAXN];
struct Trie{
int ch[1000][Siz];
int val[1000];
int fail[MAXN];
int idx;
int f[MAXN];
Trie()
{
memset(ch,0,sizeof ch);
memset(fail,0,sizeof fail);
idx=0;
}
void ins(char *s)
{
int n=strlen(s);
int u=0;
for(int i=0;i q;
q.push(0);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i
Secret Messages
- 题意:给出 (N) 个信息串, (M) 个密码串.对于每个密码串,回答有几个信息串与它满足一个是另一个的前缀.
- 将信息串插入 (Trie) 树中,记录子树大小.询问时,是密码串前缀的,在搜索过程中会经过,密码串是它的前缀的,到最后一个字符处加上子树大小即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=5e5+10;
struct Trie{
int idx;
int ch[MAXN][2];
int siz[MAXN];//计算自身的子树大小
int val[MAXN];
Trie()
{
idx=0;
memset(ch,0,sizeof ch);
memset(siz,0,sizeof siz);
memset(val,0,sizeof val);
}
void ins(int n)
{
int u=0;
for(int i=1;i<=n;++i)
{
int k=read();
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
++val[u];
}
void dfs(int u)
{
siz[u]=val[u];
for(int i=0;i<=1;++i)
if(ch[u][i])
{
dfs(ch[u][i]);
siz[u]+=siz[ch[u][i]];
}
}
int solve(int n)
{
int u=0,res=0,flag=0;
for(int i=1;i<=n;++i)
{
int k=read();
if(!ch[u][k])
flag=1;
if(flag)
continue;
u=ch[u][k];
if(i!=n)
res+=val[u];
else
res+=siz[u];
}
return res;
}
}T;
int main()
{
int n=read(),m=read();
for(int i=1;i<=n;++i)
{
int len=read();
T.ins(len);
}
T.dfs(0);
for(int i=1;i<=m;++i)
{
int len=read();
int ans=T.solve(len);
printf("%d
",ans);
}
return 0;
}
背单词
-
题意:给你 (n) 个字符串,不同的排列有不同的代价,代价按照如下方式计算(字符串 (s) 的位置为 (x) ):
-
1.排在 (s) 后面的字符串有 (s) 的后缀,则代价为$ n^2$ ;
-
2.排在 (s) 前面的字符串有 (s) 的后缀,且没有排在 (s) 后面的 (s) 的后缀,则代价为 (x-y) ((y) 为最后一个与 (s) 不相等的后缀的位置);
-
3.(s) 没有后缀,则代价为(x) 。
求最小代价和。
-
-
将所有字符串翻转插入一颗 (Trie) 树中,则关于后缀的问题全部转化为前缀.
-
容易发现,我们一定可以避免 (1) 情况的出现( (DAG) 图拓扑排序),且避免后代价一定更优.最坏也只有(frac{n(n+1)}{2} < n^2).若在位置 (0) 加上一个虚拟根作为所有字符串的前缀,那么情况 (3) 可以看做是 (2) 的特殊情况,可以一起处理.
-
将每个字符串作为一个节点,给节点编号,求节点编号与父亲编号差的最小值即可.
-
贪心地做, (dfs) 按照子树大小从小到大选出来先编号即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXL=510010;
const int Siz=26;
const int MAXN=1e5+10;
int tot=0;
int cnt=0,head[MAXN],to[MAXN<<1],nx[MAXN<<1];
inline void add(int u,int v)
{
++cnt;
to[cnt]=v;
nx[cnt]=head[u];
head[u]=cnt;
}
struct Trie{
int idx;
int ch[MAXL][Siz];
int val[MAXL];
Trie()
{
idx=0;
memset(ch,0,sizeof ch);
memset(val,0,sizeof val);
}
void ins(char buf[],int n,int v)
{
int u=0;
for(int i=n-1;i>=0;--i)
{
int k=buf[i]-'a';
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
val[u]=v;
}
void build_graph(int u,int pre)
{
++tot;
if(val[u])
add(pre,val[u]),pre=val[u];
for(int i=0;i pii;
pii tmp[MAXN];
stack stk;
void solve(int u,int fa)
{
int pos=0;
vis[u]=++tot;
ans+=vis[u]-vis[fa];
for(int i=head[u];i;i=nx[i])
{
int v=to[i];
tmp[++pos]=make_pair(-siz[v],v);
}
sort(tmp+1,tmp+1+pos);
for(int i=1;i<=pos;++i)
stk.push(tmp[i].second);
while(pos--)
{
int v=stk.top();
stk.pop();
solve(v,u);
}
}
int main()
{
int n=read();
for(int i=1;i<=n;++i)
{
scanf("%s",buf);
T.ins(buf,strlen(buf),i);
}
T.build_graph(0,0);
dfs(0);
solve(0,0);
printf("%lld
",ans);
return 0;
}
The XOR-longest Path
- 题意:给一颗边权树,求出树上最长的异或和路径.
- 利用异或的优秀性质,可以处理出节点 (1) 到每个点的距离 (dis) ,那么 (u) 和 (v) 之间的异或和距离直接就是 (dis[u]) ^ (dis[v]) .被重复计算的部分自身异或两次抵消了.
- 那么将 (dis) 数组求出后,问题就变为在这个数组中找两个数,使得这对数异或值最大.
- 使用 (Trie) 树的经典做法解决即可.
View code
#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
int out=0,fh=1;
char jp=getchar();
while ((jp>'9'||jp<'0')&&jp!='-')
jp=getchar();
if (jp=='-')
{
fh=-1;
jp=getchar();
}
while (jp>='0'&&jp<='9')
{
out=out*10+jp-'0';
jp=getchar();
}
return out*fh;
}
const int MAXN=1e5+10;
int head[MAXN],nx[MAXN<<1],to[MAXN<<1],val[MAXN<<1],cnt=0;
inline void add(int u,int v,int w)
{
++cnt;
nx[cnt]=head[u];
to[cnt]=v;
val[cnt]=w;
head[u]=cnt;
}
int n;
int dis[MAXN];
struct Trie{
int idx;
int ch[MAXN*10][2];
Trie()
{
memset(ch,0,sizeof ch);
idx=0;
}
void ins(int x)
{
int u=0;
for(int i=31;i>=0;--i)
{
int k=(x>>i)&1;
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
}
int maxxor(int x)
{
int u=0,res=0;
for(int i=31;i>=0;--i)
{
int k=(x>>i)&1;
if(ch[u][k^1])
{
res<<=1;
res+=1;
u=ch[u][k^1];
}
else
{
res<<=1;
u=ch[u][k];
}
}
return res;
}
}T;
void dfs(int u,int fa)
{
for(int i=head[u];i;i=nx[i])
{
int v=to[i];
if(v==fa)
continue;
dis[v]=dis[u]^val[i];
dfs(v,u);
}
}
int main()
{
n=read();
for(int i=1;i
小结
- 将数字按照二进制位插入 (Trie) 树形成的东西应该是叫 (01 Trie) 树...这个东东可以用来解决异或有关问题,还可以做到可持久化来解决区间问题(形态不改变,类似于主席树).若维护出节点 (siz) 后,可以解决平衡树的一类问题.涉及范围比较广泛,但大多还是只在解决异或问题时使用,因为其他时候我们可以使用我们更熟悉的数据结构.
- 可持久化的 (Trie) 树实现
咕了以后来补. - (Trie) 树的可持久化一般也只用于解决数字问题.