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

(;)
前置知识:线段树。
相关知识:可持久化Trie,具体可以看我的博客。其中对可持久化的概念有具体说明。
这里先介绍一个东东————权值线段树
(;)

权值线段树

(;)
权值线段树:顾名思义,是以数值为下标建立的一颗线段树,大概类似于,如图:

(;)
在这个例子中,我们插入了(1,2,2,2,3,3,5,6,6)这些数,因此可以发现,权值线段树中一个节点的数值即为:在这个节点的值域中数的数量。
(;)

主席树

(;)
根据可持久化的思想:每次只记录发生变化的部分。而以区间和为例,每次修改至多会使(log(n))个区间所对应的节点发生变化。
因此对于每个发生变化的节点(p),创建一个新版本(p'),而只要(p)不是叶子节点,它的左右儿子之一必定发生了更新。不妨设变化的是左儿子,那么我们只需把右儿子的信息拷贝过来,再向左子树(lson)递归,返回时令(p')的左儿子为更新后的(lson')。看下面的例子:
(;)

(;)
我们把(4)这个位置修改了,会使([4,4],[4,5],[4,6],[1,6])(4)个节点所对应的区间发生变化,可以对照参考一下。
由于主席树不再是一颗完全二叉树,所以我们不能用(2x,2x+1)这样的层次序编号,而是改为记录左右儿子的编号,其实与Trie中的字符指针类似。所以在插入查询操作时,我们要把(l,r)(这个区间对应的左右端点),当作函数的参数传递
因为它每次修改只会新增(log(n))个节点,所以主席树的空间复杂度为:(O(n+m;log;n)),其中(m)为版本个数
而对于第(i)个版本的线段树,从它的根(root[i])向下遍历,即可得到这个版本所对应的信息。
下面看看主席树可以解决什么问题。
(;)

Problem 1 ————静态区间第k小

题意

(;)
给定一个长度为(n)的序列,共有(m)次询问,每次询问给定(l,r,k),求([l,r])这段区间的第(k)小数。
(n,mleq 10^5,a[i]leq 10^9)
题目地址: https://www.luogu.com.cn/problem/P3834
暴力显而易见的超时。相信大家也都会写,这里不多赘述。
(;)

整体二分

(;)
首先,我们不考虑(l,r)的限制,在([1,n])的区间如何找第(k)小数?
容易想到我们可以建立一颗权值线段树。而我们在查询的过程中,只需判断([L,mid])这个值域中数的数量(cnt)(k)的大小关系:
(cntgeq k),显然第(k)小数在([L,mid])这段值域中。
否则说明是在([mid+1,R])这段值域中。
而这里要注意的是,由于(a[i])很大,所以我们先将其离散化,再进行操作。
(;)

前缀和

(;)
但现在有(l,r)的限制,如何做?
于是我们可以建立一颗主席树,在输入这个序列的时候不断新建版本,可以发现第(i)个版本的线段树维护的是([1,i])这段区间的信息。
另外,还有一个重要的性质:主席树的每个版本的对值域的划分是相同的
换句话来说:除了节点信息(cnt)发生变化,两颗线段树的内部结构,与和每个节点所代表的值域区间完全对应
所以我们可以发现:第(r)版本的值域区间([L,R])(cnt)减去第(l-1)版本的值域区间([L,R])(cnt)。即为:(a[l-r])中有多少个数是落在值域([L,R])中的。那么我们只需从(root[l-1])(root[r])同时出发进行访问,利用这种前缀和的性质,即可算出答案。
(;)

code

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 200010;

int n, m, idx, a[N], root[N];
vector <int> G;

struct Segtree
{
	int l, r, cnt;
}tree[N * 25]; //注意:每次新建一个版本都要新增log(n)个节点,因此空间要开够 

int Getit(int x)
{
	return lower_bound(G.begin(), G.end(), x) - G.begin();
	//找到x这个数在序列中的排名 
}

int Insert(int Last,int l,int r,int x)
{
	int Now = ++ idx; // 新建一个节点 
	tree[Now] = tree[Last]; //先把原先的拷贝过来 
	if(l == r) // 若为叶子节点 
	{
		tree[Now].cnt ++; //这个位置上数的数量 + 1 
		return Now; //返回这个点的编号 
	}
	int mid = (l + r) >> 1;
	if(x <= mid)
	{
		tree[Now].l = Insert(tree[Last].l, l, mid, x);
		// 若修改的在左子树,则往左子树递归插入新节点 
	}
	else
	{
		tree[Now].r = Insert(tree[Last].r, mid + 1, r, x);
		//否则就在右子树 
	}
	tree[Now].cnt = tree[tree[Now].l].cnt + tree[tree[Now].r].cnt;
	//push up 
	return Now;
}

int Query(int Last,int Now,int l,int r,int k)
{
	if(l == r)
	{
		return l; //若为叶子节点,则第k小的数显然就是这个 
	}
	int mid = (l + r) >> 1;
	int ch = tree[tree[Now].l].cnt - tree[tree[Last].l].cnt;
	//[l,r]这段区间在左子树值域的数量 
	if(k <= ch) // 若左子树的数量已至少有k个了,往左子树递归 
		return Query(tree[Last].l, tree[Now].l, l, mid, k);
	else //否则就往右子树递归,注意在右子树中的排名为 k - ch (左子树已有ch个了) 
		return Query(tree[Last].r, tree[Now].r, mid + 1, r, k - ch);
}
int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
	{
		scanf("%d",&a[i]);
		G.push_back(a[i]);
	}
	
	sort(G.begin(),G.end());
	G.erase(unique(G.begin(),G.end()),G.end()); // 离散化 
	
	for(int i = 1; i <= n; i++)
	{
		root[i] = Insert(root[i - 1], 0, G.size() - 1, Getit(a[i]));
		//每次输入插入一个新的版本 
	}
	 
	while(m--)
	{
		int l, r, k;
		scanf("%d%d%d",&l,&r,&k);
		printf("%d
",G[Query(root[l - 1], root[r], 0, G.size() - 1, k)]);
		//对于每次询问输出这个排名为k的数值(在G里存储的) 
	}
	return 0; 
}

(;)

Problem 2 ————动态区间第k小

题意

(;)
给定一个含有(n)个数的序列(a_1,a_2,cdots,a_n),需要支持两种操作
1.(;Q;l;r;k):表示查询下标在区间([l,r])中第(k)小的数
2.(;C;x;y):表示将(a_x)改为(y)
(n,mleq 10^5,a_ileq 10^9)
题目地址: https://www.luogu.com.cn/problem/P2617
(;)
与上道题相比只多了修改操作(废话)
(;)

树状数组维护

(;)
我们考虑把(a_x)改为(y)的影响,设(a_x)原先等于(v),显然,第(x)(n)个版本的线段树下标为(y)的值要(+1),为(v)的要(-1)
如果只是单纯的(for(x;to;n)),然后对这些线段树进行修改的话,很明显:每次修改的复杂度是(O(n;log;n)),而(m)次操作,时间复杂度为(O(nm imes log;n)),显然不行。
所以我们考虑如何优化这个东西。
与上题相同我们想要知道的是(sum[r]-sum[l-1])这样的区间和
如果仔细观察你就可以发现,这个东西可以用树状数组的思路来做。
在这里:以(root[x])为根的线段树存储的不再是([1,x])的信息了,而是([x-lowbit(x)+1,x])中的信息。
而根据树状数组的套路,每次修改会对(log(n))个点产生影响,查询也是同理。
通俗点说:就是把每颗线段树当做一个点,这样就形成了一个长度为(n)序列,然后再用树状数组维护这个序列的修改查询操作。(外层是树状数组,内层是线段树)
而每次至多修改(log(n))个点(线段树),每个线段树修改的复杂度为(log(n))
由此可得,时间复杂度:(O(m imes log_2 n))
而空间复杂度?由于每次修改都至多新建(log_2 n)个点,所以为(O((n+m) imes log_2 n))
剩下的东西就和Problem 1完全一样了。
(;)

code

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, q, a[N], len, ask_x[N], ask_y[N], ask_k[N], tmp1[100], tmp2[100]; 
int root[N << 1], idx, cnt1, cnt2, g[N << 1];
struct Node
{
	int l, r, cnt;
}tree[N * 400]; //注意要开到O((n+m) log2 n)的大小
int lowbit(int x)
{
	return x&-x;
}
int Getit(int x)
{
	return lower_bound(g + 1, g + len + 1, x) - g;
}
int Update(int Last,int l,int r,int x,int v)
{
	//函数的意义:对线段树中下标为x的值加上v 
	int Now = ++idx; tree[Now] = tree[Last];
	if(l == r)
	{
		tree[Now].cnt += v;
		return Now;
	}
	int mid = (l + r) >> 1;
	if(x <= mid)
	{
		tree[Now].l = Update(tree[Now].l, l, mid, x, v);
	}
	else tree[Now].r = Update(tree[Now].r, mid + 1, r, x, v);
	tree[Now].cnt = tree[tree[Now].l].cnt + tree[tree[Now].r].cnt;
	return Now;
}
inline void Add(int x,int val,int v)
{
	for(;x <= n; x += lowbit(x))
	{
		root[x] = Update(root[x], 1, len, val, v);
		//修改时,所有被它影响的线段树中下标为x的值都要加上v 
	}
}
int Query(int l,int r,int k)
{
	if(l == r)
	{
		return l;
	}
	int mid = (l + r) >> 1, sum = 0;
	for(register int i=1;i<=cnt1;i++)
	{
		sum -= tree[tree[tmp1[i]].l].cnt;
	}
	for(register int i=1;i<=cnt2;i++)
	{
		sum += tree[tree[tmp2[i]].l].cnt;
	}
	//sum就是维护一个前缀和,只不过使用树状数组算的 
	if(sum >= k)
	{
		for(register int i=1;i<=cnt1;i++)
		{
			tmp1[i] = tree[tmp1[i]].l;
		}

		for(register int i=1;i<=cnt2;i++)
		{
			tmp2[i] = tree[tmp2[i]].l;
		}
		return Query(l, mid, k);
	}
	else 
	{
                //跟着向下遍历
		for(register int i=1;i<=cnt1;i++)
		{
			tmp1[i] = tree[tmp1[i]].r;
		}
		for(register int i=1;i<=cnt2;i++)
		{
			tmp2[i] = tree[tmp2[i]].r;
		}
		return Query(mid + 1, r, k - sum);
	}
} 
int main()
{
	scanf("%d%d",&n,&q);
	for(register int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		g[ ++ len] = a[i];
	} 
	char op[2];
	for(register int i=1;i<=q;i++)
	{
		scanf("%s%d%d",op,&ask_x[i],&ask_y[i]);
		if(op[0] == 'C')
		{
			ask_k[i] = 0;
			g[ ++ len] = ask_y[i];
		}
		else 
		{
			scanf("%d",&ask_k[i]);
		}
	}
	sort(g + 1, g + len + 1);
	len = unique(g + 1, g + len + 1) - g - 1;
	for(register int i=1;i<=n;i++)
	{
		Add(i, Getit(a[i]), 1); //在i这个版本的线段树上a[i]离散化后的上的值加上1
	}
	for(register int i=1;i<=q;i++)
	{
		if(ask_k[i] == 0)
		{
			Add(ask_x[i], Getit(a[ask_x[i]]), -1);
			a[ask_x[i]] = ask_y[i];
			Add(ask_x[i], Getit(a[ask_x[i]]), 1);
		}
		else
		{
			cnt1 = 0, cnt2 = 0;
			int L = ask_x[i] - 1, R = ask_y[i];
			for(;L; L ^= lowbit(L)) //这里预处理出我们要算的是哪些版本的线段树
			{
				tmp1[ ++ cnt1 ] = root[L];
			} 
			for(;R; R ^= lowbit(R))
			{
				tmp2[ ++ cnt2 ] = root[R];
			}
			printf("%d
",g[Query(1, len, ask_k[i])]); //输出排名为k的值
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/czyty114/p/12870947.html