纪念我的主席树废坑

什么是主席树

主席树又叫可持久化线段树,或者函数式线段树,是最重要的可持久化数据结构之一。由于主席树不是一颗完全二叉树,所以不能再用 p < < 1 p<<1 p<<1 p < < 1 ∣ 1 p<<1|1 p<<11这种标点方式记录左右子节点的编号。这时候左右叶子节点编号就需要每次直接记录,这点与 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 ttoteteateniininn 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

一道字典树板子题

洛谷P5755 [NOI2000] 单词查找树

题目描述

在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里。为了提高查找和定位的速度,通常都要画出与单词列表所对应的单词查找树,其特点如下:

根节点不包含字母,除根节点外每一个节点都仅包含一个大写英文字母;
从根节点到某一节点,路径上经过的字母依次连起来所构成的字母序列,称为该节点对应的单词。单词列表中的每个词,都是该单词查找树某个节点所对应的单词;
在满足上述条件下,该单词查找树的节点数最少。
例:图一的单词列表对应图二的单词查找树
在这里插入图片描述
对一个确定的单词列表,请统计对应的单词查找树的节点数(包括根节点)

输入格式

一个单词列表,每一行仅包含一个单词。每个单词仅由大写的英文字符组成,长度不超过 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) (n1)行,每行有两个整数 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,q102, 1 ≤ u , v , x , y ≤ n 1≤u,v,x,y≤n 1u,v,x,yn, 1 ≤ o p ≤ 2 1≤op≤2 1op2, 1 ≤ v i 1≤v_i 1vi, 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作为指针记录下标,保留了历史版本而不是直接进行修改。


一道主席树的板子题

洛谷P3834【模板】可持久化线段树 2(主席树)

题目背景

这是个非常经典的主席树入门题——静态区间第 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)]);
	}
}

先发布一下,方便学长帮我看看,每天写一点,如果中间有什么问题请指出。

参考文献
《算法竞赛进阶指南》


2021/4/27

此坑已废,但是不想删,保留错误的自学过程为纪念

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值