首页 > 其他分享 >可持久化Trie and 线段树常见扩展

可持久化Trie and 线段树常见扩展

时间:2022-11-30 22:47:57浏览次数:74  
标签:rt 持久 Trie 线段 int build return root

可持久化数据结构

可持久化Trie

思想概述

可持久化数据结构,是一种对原本数据结构进行的扩展,可以支持查询以前的历史版本的信息
在进行每一次操作的时候,我们都把需要更新的信息的节点全部创建一个新版本,起到节省空间的效果
关于这个算法的思想,就不多说了,书上有,给张照片就是

综合运用

众所周知,Tire树用于求异或最大值的操作,那么可持久化的Tire还可以支持求区间异或最大值操作,即给定一个值\(x\),求在指定区间\([l,r]\)中找一个值,使得这个值异或\(x\)最大

下面我们讨论如何完成这个操作

首先将\(a[i],i\in [1,n]\)全部按顺序插入Tire并创建历史版本,那么对于区间\([l,r]\)来说,对于保证右端点\(r\),就只需要在\(r\)的历史版本中查询即可,我们的问题就变为了如何保证左端点不小于\(l\),对于每一个节点维护一个值\(biggest\),表示该节点及其子树下面最大的一个数的编号,那么我们只需要在每一次进入分支的时候判断是否有\(biggest\ge l\),有就可以进去,否则不可以

这个玩意本身支持在尾部进行增加

若是要求连续的异或和,可以转为异或前缀和做

下面来个实际应用
Fotile 模拟赛L
\(FOTILE\) 得到了一个长为 \(N\) 的序列 \(A\),为了拯救地球,他希望知道某些区间内的最大的连续 \(XOR\) 和。

即对于一个询问,你需要求出 \(max(A_i\) \(xor\) \(A_{i+1}\) \(xor\) \(A_{i+2}\) … \(xor\) \(A_j)\),其中 \(l≤i≤j≤r\)。

为了体现在线操作,对于一个询问 \((x,y)\):

\(l=min(((x+lastans)\bmod N)+1,((y+lastans)\bmod N)+1)\)
\(r=max(((x+lastans)\bmod N)+1,((y+lastans)\bmod N)+1)\)
其中 \(lastans\) 是上次询问的答案,一开始为 0。

题意简述,求区间最大异或子段和,强制在线

做法:

首先我们会发现,由于求子段和的原因,我们肯定不能直接上一个可持久化Trie,应该是需要嵌套其他来达成目的

但,对于异或的维护,貌似没有什么数据结构可以优化,但,一瞅题目时空限制,此题就变成了一道小清新分块题。具体的,先将序列搞成前缀异或和,再将整个序列分为\(tot\)块,然后设\(F[i,j]\)表示块\(i-j\)的答案,那么我们最终的答案一定就是:在枚举\([l,L)\)枚举一个左端点,用可持久化Trie求异或最大值,记这个的最大值为\(A\),同理在\((R,r]\)枚举一个右端点,用可持久化Trie求异或最大值,记这个最大值为\(B\),我们就成功维护了可能涉及到\([l,L),(R,r]\)区间的可能答案,那么最终答案就是\(\max\lbrace A,B,F[pos(l)+1,pos(r)-1]\rbrace\),当然,若\(pos(r)-pos(l)\le 1\)就直接暴力了

需要注意的是,我们一次枚举左端点,一次枚举右端点,故我们需要两颗树分别维护这两个操作,一个是异或前缀和,一个是异或后缀和

那么预处理\(F\)该如何呢,我们可以使用递推的思想,首先枚举整块,直接\(O(n)\)暴力枚举左右端点,然后进行递推,对于\(F[i,j]->F[i,j+1]\),我们应该在\([L(j+1),R(j+1)]\)中枚举右端点找最大值(左端点为\(L(i)\)),与\(F[i,j]\)比较,若大于则更换

若我们取\(tot=\sqrt{n}\),则总复杂度为\(O(n\sqrt{n}\log V)\)

#include<bits/stdc++.h>
using namespace std;
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dwn(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
#define all(x) (x).begin(), (x).end()
#define x first
#define y second
using pii = pair<int, int>;
using ll = long long;
#define int long long
inline void read(int &x){
    int s=0; x=1;
    char ch=getchar();
    while(ch<'0' || ch>'9') {if(ch=='-')x=-1;ch=getchar();}
    while(ch>='0' && ch<='9') s=(s<<3)+(s<<1)+ch-'0',ch=getchar();
    x*=s;
}
const int N=2e4+50, M=31, BLK=sqrt(N+1);
int n, Q;
int w[N];
struct LTrie{
	int tr[N*M][2],root[N],s[N], mxid[N*M],idx;
	int sum(int l, int r){
		return s[r]^s[l-1];
	}
	void insert(int i, int k, int p, int q){
		if(k==-1) return mxid[q]=i, void();
		int val=s[i]>>k&1;
		if(p) tr[q][val^1]=tr[p][val^1];
		tr[q][val]=++idx;
		insert(i, k-1, tr[p][val], tr[q][val]);
		mxid[q]=max(mxid[tr[q][0]], mxid[tr[q][1]]);
	}
	void build(){
		mxid[0]=-1;
		root[0]=idx=1;
		insert(0, M, 0, root[0]);
		rep(i,1,n) s[i]^=s[i-1];
		
		rep(i,1,n){
			root[i]=++idx;
			insert(i, M, root[i-1], root[i]);
		}
	}
	int query(int u, int c, int lim){
		dwn(i,M,0){
			int val=c>>i&1;
			if(mxid[tr[u][val^1]]>=lim) u=tr[u][val^1];
			else u=tr[u][val];
		}
		return s[mxid[u]]^c;
	}
	int query(int l, int r){ // fix r and query limit >= l-1
		return query(root[r], s[r], l-1);	
	}
}pre, suf;
int Len,tot,L[BLK],R[BLK],id[N]; // the block id of index
int f[BLK][BLK];
void init(){
	tot=n/Len;
	rep(i,1,tot) L[i]=(i-1)*Len+1, R[i]=i*Len;
	if(R[tot]<n) ++tot, L[tot]=R[tot-1]+1, R[tot]=n;
	rep(i,1,tot) rep(j,L[i],R[i]) id[j]=i;
	// dp
	rep(i,1,tot){
		int l=L[i], r=R[i];
		rep(j,l,r) rep(k,j,r) f[i][i]=max(f[i][i], pre.sum(j, k));
	}
	rep(len,2,tot){
		rep(l,1,tot-len+1){ // l, r is block id
			int r=l+len-1;
			int lx=L[r], rx=R[r];
			rep(i,lx,rx) f[l][r]=max(f[l][r], pre.query(L[l], i));
			f[l][r]=max(f[l][r], f[l][r-1]);
		}
	}
}
int query(int l, int r){
	int res=0;
	int lid=id[l], rid=id[r];
	if(lid==rid){
		rep(i,l,r) res=max(res, pre.query(l, i));
		return res;
	}
	if(lid+1<rid) res=f[lid+1][rid-1];
	rep(i,l,L[lid+1]-1) res=max(res, suf.query(n-r+1, n-i+1));
	rep(i,R[rid-1]+1,r) res=max(res, pre.query(l, i));
	return res;
}
signed main(){
	cin>>n>>Q;
	rep(i,1,n) read(w[i]);
	rep(i,1,n){
		pre.s[i]=w[i];
		suf.s[i]=w[n+1-i];
	}
	pre.build(), suf.build();
	Len=sqrt(n+1);
	init();
	int res=0;
	while(Q--){
		int l, r; read(l), read(r);
		l=(l+res)%n+1;
		r=(r+res)%n+1;
		if(l>r) swap(l, r);
		cout<<(res=query(l, r))<<endl;
	}
	return 0;
}

可持久化线段树

可持久化线段树,又称函数式线段树,是最重要的可持久化数据结构之一
与可持久化Trie差不多的是,可持久化线段树也是在路径上创建新的版本,不过为了空间的需要,可持久化线段树一般就不初始建树了,直接动态开点
并且需要注意的是,可持久化线段树就不支持区间修改了,除非标记永久化

思想概述

大致思想与线段树基本一致

主席树(可持久化权值线段树)

可以使用主席树解决静态区间第K小问题,这里因蓝书上有思路,就不写了,给个代码即可

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+50;
int cnt[N<<5],root[N],lc[N<<5],rc[N<<5],tot,a[N],b[N],c[N],n,m;
#define mid (l+r>>1)
#define lson lc[rt],l,mid
#define rson rc[rt],mid+1,r
void build(int &rt,int l,int r){
	rt=++tot;
	if(l==r){cnt[rt]=0;return ;}
	build(lson);build(rson);
}
int find(int p,int rt,int l,int r,int k){
	if(l==r)return l;
	int sum=cnt[lc[rt]]-cnt[lc[p]];
	if(k<=sum)return find(lc[p],lson,k);
	else return find(rc[p],rson,k-sum);
}
void insert(int p,int &rt,int l,int r,int x){
	rt=++tot;
	lc[rt]=lc[p],rc[rt]=rc[p],cnt[rt]=cnt[p]+1;
	if(l==r)return ;
	if(x<=mid)insert(lc[p],lson,x);
	else insert(rc[p],rson,x);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		c[i]=a[i];
	}
	sort(a+1,a+n+1);
	int sum=unique(a+1,a+n+1)-a-1;
	for(int i=1;i<=n;i++){
		b[i]=lower_bound(a+1,a+sum+1,c[i])-a;
	} 
	build(root[0],1,sum);
	for(int i=1;i<=n;i++)insert(root[i-1],root[i],1,sum,b[i]);
	while(m--){
		int l,r,k;
		scanf("%d%d%d",&l,&r,&k);
		printf("%d\n",a[find(root[l-1],root[r],1,sum,k)]); 
	} 
}

可持久化数组

如题,你需要维护这样的一个长度为 N 的数组,支持如下几种操作

在某个历史版本上修改某一个位置上的值

访问某个历史版本上的某一位置的值

此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

根据题意,我们使用可持久化线段树来维护,对于修改操作,我们就像普通线段树的update一样,但是边查询边创建新的节点,对于访问某历史版本的值就正常find即可,从root[x]找版本x上的值

#include<iostream>
#include<cstdio>
using namespace std;
int root[1000004], cnt;
struct node {
	int sum, l, r, lc, rc;
}t[100000005];
void build(int& s, int l, int r) {
	s = ++cnt;
	t[s].l = l, t[s].r = r;
	if (l == r){cin>>t[s].sum; return;}
	int mid = l + r >> 1;
	build(t[s].lc, l, mid);
	build(t[s].rc, mid + 1, r);
	t[s].sum = t[t[s].lc].sum + t[t[s].rc].sum;
}
void update(int &s, int pos, int dis, int k) {
	s = ++cnt;
	t[s] = t[pos];
	if (t[s].l == dis && t[s].r == dis) {
		t[s].sum = k;
		return;
	}
	if (dis >= t[t[s].lc].l && dis <= t[t[s].lc].r) {
		update(t[s].lc, t[pos].lc, dis, k);
	}
	else {
		update(t[s].rc, t[pos].rc, dis, k);
	}
	t[s].sum = t[t[s].lc].sum + t[t[s].rc].sum;
}
int find(int pos, int dis) {
	int s = pos;
	if (t[s].l == dis && t[s].r == dis)return t[s].sum;
	if (dis >= t[t[s].lc].l && dis <= t[t[s].lc].r) {
		return find(t[s].lc, dis);
	}
	else {
		return find(t[s].rc, dis);
	}
}
int main() {
	int n, m;
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> m;
	build(root[0], 1, n);
	int tot = 0;
	while (m--) {
		int opt, x, y, z;
		cin >> x >> opt;
		if (opt == 1) {
			cin >> y >> z;
			update(root[++tot], root[x], y, z);
		}
		else {
			root[++tot] = root[x];
			cin >> y;
			printf("%d\n", find(root[x], y));
		}
	}
	return 0;
}

可持久化并查集

对于可持久化的并查集,我们需要支持的操作有
1 a b 合并 a,b 所在集合;

2 k 回到第 k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;

3 a b 询问 a,b 是否属于同一集合,如果是则输出 1,否则输出 0。
结合可持久化数组的思想,我们只需要让可持久化数组支持单点修改和查询就可,但很明显不会这么简单

我们知道,并查集的常见优化有两种:路径压缩和按秩合并,这里我们采用按秩合并进行优化,路径压缩会炸空间

那么我们就需要维护按秩合并所需要的\(dep\)数组,于是我们就需要支持两个的可持久化

下面解释一下代码

建树:

    #define Mid ((l+r)>>1)
    #define lson L[rt],l,Mid
    #define rson R[rt],Mid+1,r
    // 整个代码的三个宏定义
    void build(int &rt,int l,int r){
        rt=++cnt;
        if(l==r){fa[rt]=l;return ;}
        build(lson);build(rson);
    }
// 就是普通的可持久化数组构建法,不过维护的是Fa而已

合并

	void merge(int p,int &rt,int l,int r,int x,int val){
		rt=++tot;
		L[rt]=L[p],R[rt]=R[p];
		if(l==r){
			fa[rt]=val;
			dep[rt]=dep[p];
			return ;
		}
		if(x<=mid)merge(L[p],lson,x,val);
		else merge(R[p],rson,x,val);
	}

修改节点深度

    void update(int rt,int l,int r,int pos){
        if(l==r){dep[rt]++;return ;}
        if(pos<=Mid)update(lson,pos);
        else update(rson,pos);
    }
    // 可持久化数组普通操作

查询某个值在可持久化数组里的下标

    int query(int rt,int l,int r,int pos){
        if(l==r)return rt;
        if(pos<=Mid)return query(lson,pos);
        else return query(rson,pos);
    }
    // 为了找祖先的操作

找祖先:

    int find(int rt,int pos){
        int now=query(rt,1,n,pos);
        if(fa[now]==pos)return now;
        return find(rt,fa[now]);
    }
    // 暴力找祖先

完整代码

#include<iostream>
#include<cstdio>
using namespace std;
#define N 300500
int root[N],L[N<<5],R[N<<5],fa[N<<5],dep[N<<5];
int n,m,tot;
namespace bcj{
	#define mid ((l+r)>>1) 
	#define lson L[rt],l,mid
	#define rson R[rt],mid+1,r
	void build(int &rt,int l,int r){
		rt=++tot;
		if(l==r){fa[rt]=l;return ;}
		build(lson),build(rson);
	}
	void merge(int p,int &rt,int l,int r,int x,int val){
		rt=++tot;
		L[rt]=L[p],R[rt]=R[p];
		if(l==r){
			fa[rt]=val;
			dep[rt]=dep[p];
			return ;
		}
		if(x<=mid)merge(L[p],lson,x,val);
		else merge(R[p],rson,x,val);
	}
	void update(int rt,int l,int r,int x,int d){
		if(l==r){dep[rt]+=d;return;}
		if(x<=mid)update(lson,x,d);
		else update(rson,x,d);
	}
	int query(int rt,int l,int r,int x){
		if(l==r)return rt;
		if(mid<x)return query(rson,x);
		else return query(lson,x); 
	}
	int find(int rt,int x){
		int now=query(rt,1,n,x);
		if(x==fa[now])return now;
		else return find(rt,fa[now]);
	}
	#undef mid
	#undef lson
	#undef rson
}
using namespace bcj;
int main(){
	scanf("%d%d",&n,&m);
	build(root[0],1,n);
	int ans=0;
	for(int i=1;i<=m;i++){
		int op,x,y;
		scanf("%d",&op);
		if(op==1){
			scanf("%d%d",&x,&y);
		//	x^=ans;y^=ans;
			root[i]=root[i-1];
			int pos1=find(root[i],x);
			int pos2=find(root[i],y);
			if(fa[pos1]==fa[pos2])continue;
			if(dep[pos1]>dep[pos2])swap(pos1,pos2);
			merge(root[i-1],root[i],1,n,fa[pos1],fa[pos2]);
			update(root[i],1,n,fa[pos2],1);
		}
		else if(op==2){
			scanf("%d",&x);
	//		x^=ans;
			root[i]=root[x];
		}
		else{
			root[i]=root[i-1];
			scanf("%d%d",&x,&y);
	//		x^=ans,y^=ans; 
			printf("%d\n",ans=fa[find(root[i],x)]==fa[find(root[i],y)]);
		}
	}
}

标签:rt,持久,Trie,线段,int,build,return,root
From: https://www.cnblogs.com/oierpyt/p/16940013.html

相关文章

  • docker仓库登录 配置insecure-registries
    错误现象Errorresponsefromdaemon:Gethttps://******:5000/v2/:http:servergaveHTTPresponsetoHTTPSclientDocker客户端配置-一种方式即可配置完记得重......
  • 实现Trie树-Python
    #实现Trie树:字典套字典classTrie():def__init__(self):self.child={}definsert(self,word):current_node=self.child......
  • P3834 【模板】可持久化线段树 2
    【模板】可持久化线段树2题目背景这是个非常经典的可持久化权值线段树入门题——静态区间第\(k\)小。数据已经过加强,请使用可持久化权值线段树。同时请注意常数优化......
  • NOTE_pinia持久化插件的使用
    E:\song\vue_vue_learn\vite-project\index.html<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><linkrel="icon"type="image/svg+xml"......
  • 【小航的算法日记】线段树
    本内容取经于:https://leetcode.cn/problems/my-calendar-i/solution/by-lfool-xvpv/一、概念概念区分:线段树解决的是「区间和」的问题,且该「区间」会被修改前缀和解决的是......
  • 3.5 Docker最新入门教程-Docker入门-持久化数据库
    3.5持久化数据库您是否注意到,每次我们启动容器时,我们的待办事项列表都会被清除干净。为什么是这样?让我们深入了解容器的工作原理。容器的文件系统当容器运行时,它使用镜......
  • [Typescript] 120. Hard - ObjectFromEntries
    Implementthetypeversionof Object.fromEntriesForexample:interfaceModel{name:string;age:number;locations:string[]|null;}typeModelEnt......
  • 线段树合并算法思路
    线段树合并,听起来很高端,其实就是把两棵线段树相加。引用一下一位大佬的图:具体地说,每次合并操作我们考虑将\(o_2\)这棵树的信息加到\(o_1\)上,那么我们就遍历二者区间......
  • [模板] 线段树
    线段树template<classT,classF>classSegmentTree{constintn;vector<T>node;voidupdate(intrt,intl,intr,intpos,Ff){if(r......
  • Redis学习(一)之 持久化、主从与哨兵架构
    jiaruredis持久化RDB快照:在默认情况下,Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中。你可以对Redis进行设置,让它在“N秒内数据集至少有M个改动”......