什么是主席树
主席树又叫可持久化线段树,或者函数式线段树,是最重要的可持久化数据结构之一。由于主席树不是一颗完全二叉树,所以不能再用
p
<
<
1
p<<1
p<<1和
p
<
<
1
∣
1
p<<1|1
p<<1∣1这种标点方式记录左右子节点的编号。这时候左右叶子节点编号就需要每次直接记录,这点与
T
r
i
e
Trie
Trie的字符指针类似。
一切树状数组的操作都可以用线段树来实现,但是主席树无法实现像“最大子段和”一类的操作,并且如果需要一些比较复杂的懒标记,主席树就会很鸡肋了。
先引入下Trie
普通的Trie字典树
T
r
i
e
Trie
Trie,又称单词查找树,是一种树形结构,用于保存大量的字符串。它的优点是:利用字符串的公共前缀来节约存储空间。上限到
80
80
80万就差不多了。
T
r
i
e
Trie
Trie树主要用于查找大量公共前缀用。
在
T
r
i
e
Trie
Trie树中查找一个关键字的时间和树中包含的结点数无关,而取决于组成关键字的字符数。而二叉查找树的查找时间和树中的结点数有关
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)。
如果要查找的关键字可以分解成字符序列且不是很长,利用
T
r
i
e
Trie
Trie树查找速度优于二叉查找树。如:若关键字长度最大是
5
5
5,则利用
T
r
i
e
Trie
Trie树,利用5次比较可以从
2
6
5
26^5
265=11881376个可能的关键字中检索出指定的关键字。而利用二叉查找树至少要进行
l
o
g
2
2
6
5
=
23.5
log_226^5=23.5
log2265=23.5次比较。
它有
3
3
3个基本性质:
1
、
1、
1、根节点不包含字符,除根节点外每一个节点都只包含
1
1
1个字符,每个节点都有
26
26
26个分叉。
2
、
2、
2、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3
、
3、
3、每个节点的所有子节点包含的字符都不相同。
主要应用:
字符串排序:在字母树上先序遍历
公共前缀计算:公共祖先
字符串检索:在字母树上跑一遍即可
struct node
{
char ch; //本节点的值
bool endflag; //是否是某个单词的最后一个字符
int link[26]; //26个分叉
} Trie[600100];
//Trie[0] 是根
在这个
T
r
i
e
Trie
Trie结构中,保存了
t
、
t
o
、
t
e
、
t
e
a
、
t
e
n
、
i
、
i
n
、
i
n
n
t、to、te、tea、ten、i、in、inn
t、to、te、tea、ten、i、in、inn这
8
8
8个字符串,仅占用
8
8
8个char就存了这么多个字符串,可以说是很省空间的了。
建树的过程:
void add(int k,int node) //k是s的第k个字符,node为当前节点。
{
int chindex=s[k]-¡®A¡¯; //字符的编号
if (Trie[node].link[chindex]==0) //新开节点
{
Trie[node].link[chindex]=++len;
Trie[len].ch=s[k];
Trie[len].endflag=false;
}
int nexnode=Trie[node].link[chindex];//下一个节点的下标
if (k==(int)s.size()-1)
{
Trie[nexnode].endflag=true;
return;
}
add(k+1,nexnode);
}
检索trie树时:
bool find(int k, int last,int node)//k是要查找字符串s的第k个元素
{
int chindex=s[k]-'A';
if (Trie[node].link[chindex]==0) return false;
int nextnode=Trie[node].link[chindex];
if (k==(s.size()-1)) //如果k是最后一个字符
if (Trie[nextnode].endflag) return true;
else return false;
return find(k+1,last,nextnode);
}
1.
1.
1.若
P
P
P的
c
c
c字符指针指向空,则说明
S
S
S没有被插入过
T
r
i
e
Trie
Trie,结束检索。
2.
2.
2.若
P
P
P的
c
c
c字符指针指向一个已经存在的节点
Q
Q
Q,则令
P
=
Q
P=Q
P=Q。
当
S
S
S中的字符扫描完毕时,若当前节点
P
P
P被标记为一个字符串的末尾,则说明
S
S
S在
T
r
i
e
Trie
Trie中存在,否则说明
S
S
S没有被插入过
T
r
i
e
Trie
Trie。
一道字典树板子题
题目描述
在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里。为了提高查找和定位的速度,通常都要画出与单词列表所对应的单词查找树,其特点如下:
根节点不包含字母,除根节点外每一个节点都仅包含一个大写英文字母;
从根节点到某一节点,路径上经过的字母依次连起来所构成的字母序列,称为该节点对应的单词。单词列表中的每个词,都是该单词查找树某个节点所对应的单词;
在满足上述条件下,该单词查找树的节点数最少。
例:图一的单词列表对应图二的单词查找树
对一个确定的单词列表,请统计对应的单词查找树的节点数(包括根节点)
输入格式
一个单词列表,每一行仅包含一个单词。每个单词仅由大写的英文字符组成,长度不超过 6363 6363 6363个字符。文件总长度不超过 32 K 32K 32K,至少有一行数据。
输出格式
仅包含一个整数。该整数为单词列表对应的单词查找树的节点数。
#include<bits/stdc++.h>
using namespace std;
int end=27;
int i,f,ans=0,boyt,now;
string s;
struct lianbiao
{
char n;
int boy[27]; //因为保证全是大写,所以最多也就26个字母
}trie[60000];
int main()
{
while(cin>>s)
{
now=int(s[0]-64);//该字符串的第一个字母的int形式
trie[now].n=s[0];
for(f=1;f<s.size();f++)
{
boyt=int(s[f])-64;//字符串中每个字母的int形式
if(trie[trie[now].boy[boyt]].n==s[f])//已经有了
{
now=trie[now].boy[boyt];
}
else//如果没有
{
trie[end].n=s[f];//注意,据说一些版本中end是关键词
trie[now].boy[boyt]=end;
now=end;
end++;
}
}
}
for(i=1;i<=60000;i++)
if(trie[i].n!=0)
ans++;
cout<<ans+1;
return 0;
}
再来一道可持久化trie树的板子题
洛谷P4592 [TJOI2018]异或
题目描述
现在有一颗以
1
1
1为根节点的由
n
n
n个节点组成的树,节点从
1
1
1至
n
n
n编号。树上每个节点上都有一个权值
v
i
v_i
vi 。现在有
q
q
q次操作,操作如下:
1
1
1
x
x
x
z
z
z:查询节点
x
x
x的子树中的节点权值与
z
z
z异或结果的最大值。
2
2
2
x
x
x
y
y
y
z
z
z:查询节点
x
x
x到节点
y
y
y的简单路径上的节点的权值与
z
z
z 异或结果最大值。
输入格式
输入的第一行是两个整数,分别代表结点个数
n
n
n和询问个数
q
q
q。
第二行有
n
n
n个整数,第
i
i
i个整数表示点
i
i
i的的权值
v
i
v_i
vi
接下来
(
n
−
1
)
(n-1)
(n−1)行,每行有两个整数
u
u
u,
v
v
v,表示存在一条连结
u
u
u和
v
v
v的边。
接下来
q
q
q 行,每行首先有一个整数
o
p
op
op,代表操作类型。
若
o
p
=
1
op=1
op=1,则一个空格后有两个整数
x
x
x,
z
z
z代表查询节点
x
x
x的子树中的节点权值与
z
z
z异或结果的最大值。
若
o
p
=
2
op=2
op=2,则一个空格后有三个整数
x
x
x,
y
y
y,
z
z
z,代表查询节点
x
x
x到节点
y
y
y的简单路径上的节点的权值与
z
z
z异或结果最大值。
输出格式
对于每一个查询,输出一行一个整数代表答案。
数据范围
对于 100 100% 100的数据,保证 1 < n , q ≤ 1 0 2 1<n,q≤10^2 1<n,q≤102, 1 ≤ u , v , x , y ≤ n 1≤u,v,x,y≤n 1≤u,v,x,y≤n, 1 ≤ o p ≤ 2 1≤op≤2 1≤op≤2, 1 ≤ v i 1≤v_i 1≤vi, z < 2 30 z<2^{30} z<230。
#include<bits/stdc++.h>
#define Graph(x)for(int i=last[x],y=edge[i].ver;i;i=edge[i].next,y=edge[i].ver)
using namespace std;
int n,m,q,a[100010],root[2][100010],tot[2],dfs_clock,id[100010];
int fa[100010],siz_e[100010],dep[100010],top[100010],son[100010];
int last[100010],num;
char str[10];
struct EDGE{int ver,next;}edge[200010];
struct TIRE{int son[2],data,max;}tire[2][100010*32];
inline int read()
{
int x=0,w=0;char ch=0;
while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return w?-x:x;
}
void insert(int Id,int p,int p2,int x)
{
tire[Id][p].data=tire[Id][p2].data+1;
for(int i=30;i>=0;i--){
int ch=(x>>i)&1;
tire[Id][p].son[0]=tire[Id][p2].son[0];
tire[Id][p].son[1]=tire[Id][p2].son[1];
tire[Id][p].son[ch]=++tot[Id];
p=tire[Id][p].son[ch];
p2=tire[Id][p2].son[ch];
tire[Id][p].data=tire[Id][p2].data+1;
}
}
int ask(int Id,int p1,int p2,int p3,int p4,int x,int y=0)
{
if(!Id){
for(int i=30;i>=0;i--){
int ch=(x>>i)&1;
if(tire[Id][tire[Id][p1].son[ch^1]].data+tire[Id][tire[Id][p2].son[ch^1]].data-tire[Id][tire[Id][p3].son[ch^1]].data-tire[Id][tire[Id][p4].son[ch^1]].data>0){
p1=tire[Id][p1].son[ch^1];
p2=tire[Id][p2].son[ch^1];
p3=tire[Id][p3].son[ch^1];
p4=tire[Id][p4].son[ch^1];
y+=1<<i;
}else{
p1=tire[Id][p1].son[ch];
p2=tire[Id][p2].son[ch];
p3=tire[Id][p3].son[ch];
p4=tire[Id][p4].son[ch];
}
}
}else{
for(int i=30;i>=0;i--){
int ch=(x>>i)&1;
if(tire[Id][tire[Id][p2].son[ch^1]].data-tire[Id][tire[Id][p1].son[ch^1]].data>0){
p1=tire[Id][p1].son[ch^1];
p2=tire[Id][p2].son[ch^1];
y+=1<<i;
}else{
p1=tire[Id][p1].son[ch];
p2=tire[Id][p2].son[ch];
}
}
}
return y;
}
void add(int U,int V){edge[++num]=(EDGE){V,last[U]};last[U]=num;}
void dfs(int x,int F)
{
siz_e[x]=1;
Graph(x){
if(y==F)continue;
dep[y]=dep[fa[y]=x]+1;
dfs(y,x);
siz_e[x]+=siz_e[y];
if(siz_e[y]>siz_e[son[x]])
son[x]=y;
}
}
void dfs(int x,int F,int Top)
{
insert(0,root[0][x]=++tot[0],root[0][fa[x]],a[x]);
id[x]=++dfs_clock;
insert(1,root[1][id[x]]=++tot[1],root[1][dfs_clock-1],a[x]);
top[x]=Top;
if(!son[x])return;
dfs(son[x],x,Top);
Graph(x){
if(y==F||y==son[x])continue;
dfs(y,x,y);
}
}
int lca(int x,int y)
{
while(top[x]!=top[y])
dep[top[x]]>dep[top[y]]?x=fa[top[x]]:y=fa[top[y]];
return dep[x]<dep[y]?x:y;
}
int main()
{
n=read();q=read();
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=1;i<n;i++){
int x=read(),y=read();
add(x,y);add(y,x);
}
dfs(1,0);
dfs(1,0,1);
while(q --> 0)
if(read()&1){
int x=read(),y=read();
printf("%d\n",ask(1,root[1][id[x]-1],root[1][id[x]+siz_e[x]-1],0,0,y));
}else{
int x=read(),y=read(),z=read(),xy=lca(x,y);
printf("%d\n",ask(0,root[0][x],root[0][y],root[0][xy],root[0][fa[xy]],z));
}
}
求 x o r xor xor的最大值,显然是相似度越低就越大,显然 s o r t sort sort和 01 T r i e 01Trie 01Trie都可做,但是这个需要修改,而且需要退回,就必须要用到可持久化的 01 T r i e 01Trie 01Trie。链上查询时,相当于 x x x到 l c a lca lca, y y y到 l c a lca lca查询,也就相当于区间问题了。
所以什么叫可持久化?
简单点说就是在需要修改时,把修改变为新建,保留历史版本,省掉完全拷贝的空间,然后用
O
(
l
o
g
n
)
O(log_n)
O(logn)的方式查找。
这个题是紫的,显然要先跳过详解
可持久化线段树的前置知识
动态开点
为了降低空间复杂度,有时我们可以不建出一整棵线段树,而是再建立一个根节点来代表整个区间,当需要访问线段树的某棵子树时,再建立代表这个子区间的节点。如开头所说,这里的下标抛弃了完全二叉树父子节点的 2 2 2倍编号规则,改用为相当于指针的变量记录左右子节点的编号。同时,它也不再保存每个节点代表的区间,而是每次递归访问过程中作为参数传递。
线段树合并
例题用不到(bushi)
可持久化线段树
[福利]可持久化线段树
题目描述
为什么说本题是福利呢?因为这是一道非常直白的可持久化线段树的练习题,目的并不是虐人,而是指导你入门可持久化数据结构。
线段树有个非常经典的应用是处理
R
M
Q
RMQ
RMQ问题,即区间最大/最小值询问问题。现在我们把这个问题可持久化一下:
Q
,
k
,
l
,
r
Q,k,l,r
Q,k,l,r查询数列在第k个版本时,区间
[
l
,
r
]
[l, r]
[l,r]上的最大值
M
,
k
,
p
,
v
M,k,p,v
M,k,p,v把数列在第k个版本时的第
p
p
p个数修改为
v
v
v,并产生一个新的数列版本
最开始会给你一个数列,作为第
1
1
1个版本。
每次
M
M
M操作会导致产生一个新的版本。修改操作可能会很多呢,如果每次都记录一个新的数列,空间和时间上都是令人无法承受的。所以我们需要可持久化数据结构:
对于最开始的版本
1
1
1,我们直接建立一颗线段树,维护区间最大值。
修改操作呢?我们发现,修改只会涉及从线段树树根到目标点上一条树链上
O
(
l
o
g
n
)
O(log_n)
O(logn)个节点而已,其余的节点并不会受到影响。所以对于每次修改操作,我们可以只重建修改涉及的节点即可。就像这样:
需要查询第
k
k
k个版本的最大值,那就从第
k
k
k个版本的树根开始,像查询普通的线段树一样查询即可。 要计算好所需空间哦
输入格式
第一行两个整数
N
,
Q
N, Q
N,Q。
N
N
N是数列的长度,
Q
Q
Q表示询问数
第二行
N
N
N个整数,是这个数列
之后
Q
Q
Q行,每行以
0
0
0或者
1
1
1开头,
0
0
0表示查询操作
Q
Q
Q,
1
1
1表示修改操作
M
M
M,格式为
0
,
k
,
l
,
r
0,k,l,r
0,k,l,r查询数列在第k个版本时,区间
[
l
,
r
]
[l,r]
[l,r]上的最大值 或者
1
,
k
,
p
,
v
1,k,p,v
1,k,p,v把数列在第k个版本时的第
p
p
p个数修改为
v
v
v,并产生一个新的数列版本
输出格式
对于每个询问,输出正确答案
样例数据
input
4 5
1 2 3 4
0 1 1 4
1 1 3 5
0 2 1 3
0 2 4 4
0 1 2 4
output
4
5
4
4
样例解释
序列版本1: 1 2 3 4
查询版本1的[1, 4]最大值为4
修改产生版本2: 1 2 5 4
查询版本2的[1,3]最大值为5
查询版本1的[4,4]最大值为4
查询版本1的[2,4]最大值为4
数据规模与约定
N
<
=
10000
,
Q
<
=
100000
N <= 10000,Q <= 100000
N<=10000,Q<=100000对于每次询问操作的版本号
k
k
k保证合法, 区间
[
l
,
r
]
[l, r]
[l,r]一定满足
1
<
=
l
<
=
r
<
=
N
1<=l<=r<= N
1<=l<=r<=N
#include<bits/stdc++.h>
using namespace std;
const int N=1000233;
int cnt,n,a[N],m,root[N];
int tot=0;
struct ST
{
int l,r,dat;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define dat(x) tree[x].dat
}tree[N*20];
int build(int l,int r)
{
int p=++cnt;
if(l==r){dat(p)=a[l];return p;}
int mid=(l+r)>>1;
l(p)=build(l,mid);r(p)=build(mid+1,r);
dat(p)=max(dat(r(p)),dat(l(p)));
return p;
}
int change(int now,int l,int r,int x,int v)
{
int p=++cnt; tree[p]=tree[now];
if(l==r) {dat(p)=v;return p;}
int mid=(l+r)>>1;
if(x<=mid) l(p)=change(l(now),l,mid,x,v);
if(x>mid) r(p)=change(r(now),mid+1,r,x,v);
dat(p)=max(dat(r(p)),dat(l(p)));
return p;
}
int ask(int p,int l,int r,int ql,int qr)
{
if(ql<=l&&qr>=r) return dat(p);
int val=-1e5,mid=(l+r)>>1;
if(ql<=mid) val=max(val,ask(l(p),l,mid,ql,qr));
if(qr>mid) val=max(val,ask(r(p),mid+1,r,ql,qr));
return val;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
root[++tot]=build(1,n);
while(m--)
{
int k,opt,l,r;
scanf("%d%d%d%d",&opt,&k,&l,&r);
if(opt==0) printf("%d\n",ask(root[k],1,n,l,r));
else root[++tot]=change(root[k],1,n,l,r);
}
}
这个算是真丶可持久化线段树的水题了,表面上看着和线段树一样,但是以 t o t tot tot作为指针记录下标,保留了历史版本而不是直接进行修改。
一道主席树的板子题
题目背景
这是个非常经典的主席树入门题——静态区间第
k
k
k小。
数据已经过加强,请使用主席树。同时请注意常数优化。
题目描述
如题,给定 n n n个整数构成的序列 a a a,将对于指定的闭区间 [ l , r ] [l, r] [l,r] 查询其区间内的第 k k k小值。
输入格式
第一行包含两个整数,分别表示序列的长度
n
n
n 和查询的个数
m
m
m。
第二行包含
n
n
n个整数,第
i
i
i个整数表示序列的第
i
i
i个元素
a
i
a_i
ai。
接下来
m
m
m行每行包含三个整数
l
l
l,
r
r
r,
k
k
k , 表示查询区间
[
l
,
r
]
[l, r]
[l,r]内的第
k
k
k小值。
输出格式
对于每次询问,输出一行一个整数表示答案。
虽然是板子,但也还是分析一下吧
主席树
#include<bits/stdc++.h>
using namespace std;
bool ks;
int n,m,stnum,a[200010],lsh[200010],root[200010];
struct SegmentTree{int data,ls,rs;}st[8000010];
bool js;
inline int read()//快读
{
int x=0,w=0;char ch=0;
while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return w?-x:x;
}
int New() {st[++stnum]=(SegmentTree){0,0,0};return stnum;}
void updct(int p) {st[p].data=st[st[p].ls].data+st[st[p].rs].data;}
void insert(int &p,int p2,int l,int r,int pos)
{
if(!p)p=New();
if(l==r){st[p].data=st[p2].data+1;return;}
int mid=(l+r)>>1;
if(pos<=mid)st[p].rs=st[p2].rs,insert(st[p].ls,st[p2].ls,l,mid,pos);
else st[p].ls=st[p2].ls,insert(st[p].rs,st[p2].rs,mid+1,r,pos);
updct(p);
}
int ask(int p1,int p2,int l,int r,int k)
{
if(!p2)return 0;
if(l==r)return l;
int mid=(l+r)>>1,tot=st[st[p2].ls].data-st[st[p1].ls].data;
if(k<=tot)return ask(st[p1].ls,st[p2].ls,l,mid,k);
return ask(st[p1].rs,st[p2].rs,mid+1,r,k-tot);
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) lsh[i]=a[i]=read();
sort(lsh+1,lsh+1+n);
int cnt=unique(lsh+1,lsh+1+n)-lsh-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh;
for(int i=1;i<=n;i++)
insert(root[i],root[i-1],1,cnt,a[i]);
while(m --> 0){
int l=read(),r=read(),k=read();
printf("%d\n",lsh[ask(root[l-1],root[r],1,cnt,k)]);
}
}
先发布一下,方便学长帮我看看,每天写一点,如果中间有什么问题请指出。
参考文献
《算法竞赛进阶指南》