zoukankan      html  css  js  c++  java
  • 可持久化线段树学习笔记

    可持久化线段树学习笔记

    可持久化线段树就是可以询问历史版本状态的线段树。

    我们考虑暴力怎样实现询问线段树的历史状态,我们每次在对线段树进行操作的时候,每次都把位修改前的版本存一下,然后要找历史版本的时候直接找,这样时间复杂度和空间复杂都会爆炸。

    我们可以发现,每次对线段树进行操作最多才会修改 (log_2(size)) 个节点,其中 (size) 为线段树的大小,所以我们每次把线段树全部复制一遍是不必要的,我们只需要把修改时经过的节点新建出来,然后将其他的节点连接了原来的线段树上就可以了。

    (like this)

    严格的来说这个东西不是一棵树,但是他如果从父亲节点往儿子节点走就是一棵树,所以对于任意一颗这样的树它还满足线段树的性质,所以我们不用纠结这个是不是一棵树。

    然后每次修改都进行一下这样的操作,要查询历史版本的时候只需要找到历史版本的根节点,然后直接当成线段树来用就可以了。

    所以我们可以发现,可持久化线段树的时间复杂度和线段树一样,建树是 (Theta (size)),单次查询是 (Theta (log_2size))(size) 维护的数据范围,空间复杂度是 (Theta (log_2n))(n) 是操作次数。

    inline void insert(int &rt,int l,int r,int x)
    {
    	t[++cnt]=t[rt];
    	rt=cnt;
    	if (l==r)
    	{
    		t[rt].sum++;
    		return ;
    	}
    	int mid=(l+r)>>1;
    	if (x<=mid) insert(t[rt].ls,l,mid,x);
    	else insert(t[rt].rs,mid+1,r,x);
    	push_up(rt);
    	return ;
    }
    

    与普通线段树不同的地方就是对 (rt) 加了取地址,表示对传入的值的原值进行操作,还有下面的这两句

    	t[++cnt]=t[rt]; //将原节点的信息复制给新建的节点的信息,不要丢失另一个不修改儿子的信息
    	rt=cnt;         //把树根转化为新建节点,再加上取地址,这样对于父亲节点来说,它的修改的儿子就是新建的节点
    

    P3834 可持久化线段树 1(主席树)

    这道题目咋一看和可持久化线段树没有任何关系,但是这确实是可持久化线段树的日常操作。

    我们考虑假设给你 (l-1)(r) 这个状态下的权值线段树,这题肯定就很好做了,只需要两颗线段树减一下就可以了,然后我们发现这就是维护一颗可持久化线段树,然后在查询历史信息。

    #include<bits/stdc++.h>
    const int N=2e5+100;
    using std::sort;
    int n,m,bsize;
    int a[N],b[N],rt[N];
    struct tree{
    	int cnt;
    	struct Tree{
    		int sum,ls,rs;
    	}t[N*20];
    	inline void clear() {cnt=0;return ;}
    	inline void push_up(int rt)
    	{
    		t[rt].sum=t[t[rt].ls].sum+t[t[rt].rs].sum;
    		return ;
    	}
    	inline void insert(int &rt,int l,int r,int x)
    	{
    		t[++cnt]=t[rt];
    		rt=cnt;
    		if (l==r)
    		{
    			t[rt].sum++;
    			return ;
    		}
    		int mid=(l+r)>>1;
    		if (x<=mid) insert(t[rt].ls,l,mid,x);
    		else insert(t[rt].rs,mid+1,r,x);
    		push_up(rt);
    		return ;
    	}
    	inline int query(int rt1,int rt2,int l,int r,int k)
    	{
    		if (l==r) return l;
    		int sz=t[t[rt2].ls].sum-t[t[rt1].ls].sum,mid=(l+r)>>1;
    		if (k<=sz) return query(t[rt1].ls,t[rt2].ls,l,mid,k);
    		else return query(t[rt1].rs,t[rt2].rs,mid+1,r,k-sz);
    	}
    }t;
    inline int midfind(int x)
    {
    	int l=1,r=bsize;
    	while (l<=r)
    	{
    		int mid=(l+r)>>1;
    		if (b[mid]==x) return mid;
    		if (b[mid]<x) l=mid+1;
    		else r=mid-1;
    	}
    	return 0;
    }
    inline int read()
    {
    	int ans=0,p=1;
    	char ch=getchar();
    	while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
    	while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
    	return ans*p;
    }
    inline void Pre()
    {
    	sort(b+1,b+n+1);
    	for (int i=1;i<=n;i++)
    	{
    		int k=i;
    		b[++bsize]=b[i];
    		while (b[k]==b[k+1]) k++;
    		i=k;
    	}
    	return ;
    }
    int main()
    {
    	n=read();m=read();
    	for (int i=1;i<=n;i++)
    	a[i]=b[i]=read();
    	Pre();
    	t.clear();
    	for (int i=1;i<=n;i++)
    	{
    		int now=midfind(a[i]);
    		rt[i]=rt[i-1];
    		t.insert(rt[i],1,bsize,now);
    	}
    	while (m--)
    	{
    		int l,r,k;
    		l=read();r=read();k=read();
    		printf("%d
    ",b[t.query(rt[l-1],rt[r],1,bsize,k)]);
    	}
    	return 0;
    }
    

    P3919 【模板】可持久化数组

    这道题目才比较像可持久化应该做的东西,对历史状态进行修改,查询历史状态,不过需要注意的是每次查询都要新建一个历史状态。

    #include<bits/stdc++.h>
    const int N=1e6+100;
    int n,m,No;
    int a[N],root[N];
    struct Tree{
    	int cnt;
    	struct tree{
    		int ls,rs,sum;
    	}t[N*20];
    	inline void clear() {cnt=1;return ;}
    	inline int query(int rt,int l,int r,int pos)
    	{
    		if (l==r) return t[rt].sum;
    		int mid=(l+r)>>1;
    		if (pos<=mid) return query(t[rt].ls,l,mid,pos);
    		else return query(t[rt].rs,mid+1,r,pos);
    	}
    	inline void update(int &rt,int l,int r,int pos,int k)
    	{
    		t[++cnt]=t[rt];
    		rt=cnt;
    		if (l==r)
    		{
    			t[rt].sum=k;
    			return ;
    		}
    		int mid=(l+r)>>1;
    		if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
    		else update(t[rt].rs,mid+1,r,pos,k);
    		return ;
    	}
    	inline void build(int rt,int l,int r)
    	{
    		if (l==r)
    		{
    			t[rt].sum=a[l];
    			return ;
    		}
    		int mid=(l+r)>>1;
    		t[rt].ls=++cnt;
    		build(t[rt].ls,l,mid);
    		t[rt].rs=++cnt;
    		build(t[rt].rs,mid+1,r);
    		return ;
    	}
    }T;
    inline int read()
    {
    	int ans=0,p=1;
    	char ch=getchar();
    	while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
    	while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
    	return ans*p;
    }
    int main()
    {
    	n=read();m=read();
    	for (int i=1;i<=n;i++)
    	a[i]=read();
    	T.clear();
    	T.build(1,1,n);
    	root[0]=1;
    	for (int i=1;i<=m;i++)
    	{
    		int vi,opt,pos,k;
    		scanf("%d%d",&vi,&opt);
    		if (opt==1)
    		{
    			scanf("%d%d",&pos,&k);
    			int nrt=root[vi];
    			T.update(nrt,1,n,pos,k);
    			root[++No]=nrt;
    		}
    		if (opt==2)
    		{
    			scanf("%d",&pos);
    			root[++No]=root[vi];
    			printf("%d
    ",T.query(root[vi],1,n,pos));
    		}
    	}
    	return 0;
    }
    

    P4137 Rmq Problem / mex

    这道题的第一个思路就是对于序列的从前到后建主席树,然后统计 ([l,r]) 每个数出现多少次,之后二分一下就可以了。

    但是这种思路是错误的,因为有一些出现不止一次的数会对二分造成影响,如果强制为把出现多次的数赋值为 (1) 的话,这样做减法又不对。

    其实正确的做法是,每次记录对于枚举到当前第 (i) 个数,记录一下前面每一个数最后出现的位置,包括自己。

    这样每次查找的时候只需要在 (R) 这个位置建的主席树里找一个最小值小于 (L) 的最短前缀就可以了。

    因为这个数在前面出现的位置小于 (L) ,所以肯定合法,因为是最小前缀,所以满足是最小的合法自然数。

    如果当前前缀的最小值大于 (L) ,所以需要增长前缀长度,因为经过预处理,所以往后肯定有合法答案,如果小于 (L) 则尽量缩小前缀的长度。

    #include<bits/stdc++.h>
    using std::min; 
    using std::sort;
    const int N=2e5+100;
    int n,m,num,size;
    int a[N],b[N<<1],root[N];
    struct Tree{
    	int cnt;
    	struct tree{
    		int ls,rs,minn;
    	}t[N*20];
    	inline void clear() {cnt=0;return ;}
    	inline void push_up(int rt)
    	{
    		t[rt].minn=min(t[t[rt].ls].minn,t[t[rt].rs].minn);
    		return ;
    	}
    	inline void update(int &rt,int l,int r,int pos,int k)
    	{
    		t[++cnt]=t[rt];
    		rt=cnt;
    		if (l==r)
    		{
    			t[rt].minn=k;
    			return ;
    		}
    		int mid=(l+r)>>1;
    		if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
    		else update(t[rt].rs,mid+1,r,pos,k);
    		push_up(rt);
    		return ;
    	}
    	inline int query(int rt,int l,int r,int nl,int nr)
    	{
    		if (l==nl&&r==nr) return t[rt].minn;
    		int mid=(l+r)>>1;
    		if (nr<=mid) return query(t[rt].ls,l,mid,nl,nr);
    		else if (nl>mid) return query(t[rt].rs,mid+1,r,nl,nr);
    		else return min(query(t[rt].ls,l,mid,nl,mid),query(t[rt].rs,mid+1,r,mid+1,nr));
    	}
    }T;
    inline int solve(int val,int rt)
    {
    	int l=1,r=num,ans=num;
    	while (l<=r)
    	{
    		int mid=(l+r)>>1;
    		if (T.query(rt,1,num,1,mid)>=val) l=mid+1;
    		else
    		{
    			r=mid-1;
    			ans=mid;
    		}
    	}
    	return b[ans];
    }
    inline int midfind(int x)
    {
    	int l=1,r=num;
    	while (l<=r)
    	{
    		int mid=(l+r)>>1;
    		if (b[mid]==x) return mid;
    		if (b[mid]<x) l=mid+1;
    		else r=mid-1;
    	}
    	return 0;
    }
    inline void Pre()
    {
    	sort(b+1,b+size+1);
    	for (int i=1;i<=size;i++)
    	{
    		int k=i;
    		b[++num]=b[i];
    		while (b[k]==b[k+1]&&k+1<=size) k++;
    		i=k;
    	}
    	return ;
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for (int i=1;i<=n;i++)
    	{
    		scanf("%d",a+i);
    		b[++size]=a[i];
    		b[++size]=a[i]+1;
    	}
    	Pre();
    	T.clear();
    	for (int i=1;i<=n;i++)
    	{
    		root[i]=root[i-1];
    		T.update(root[i],1,num,midfind(a[i]),i);
    	}
    	for (int i=1;i<=m;i++)
    	{
    		int l,r;
    		scanf("%d%d",&l,&r);
    		printf("%d
    ",solve(l,root[r]));
    	}
    	return 0;
    }
    

    然后就会发现套的二分完全没有必要,可以直接在线段树上进行操作。

    只需要把二分答案的代码换成下面的代码就可以了。

    inline int midquery(int rt,int l,int r,int L)
    {
    	if (l==r) return l;
    	int mid=(l+r)>>1;
    	if (t[t[rt].ls].minn<L) return midquery(t[rt].ls,l,mid,L);
    	else return midquery(t[rt].rs,mid+1,r,L);
    }
    
  • 相关阅读:
    2018.7.9 模拟赛
    树状数组||归并排序求逆序对+离散化 nlogn
    LCS nlogn
    孤岛营救问题
    [POJ 3621] Sighting Cows
    树状数组求LIS
    nlogn求逆序对&&陌上花开
    最长可重区间集
    LCA模板
    [BZOJ] 4196 [Noi2015]软件包管理器
  • 原文地址:https://www.cnblogs.com/last-diary/p/11632086.html
Copyright © 2011-2022 走看看