简介
主席树全称应为可持久化权值线段树,可用于解决区间第k小问题。但可能由于这个的影响力比较大,现在主席树也被泛指为可持久化线段树。
主席树树是由NOI选手黄嘉泰(HJT,咱也不敢说)首先使用的。黄嘉泰原话:“这种求k 大的方法(函数式线段树)应该是我最早开始用的”。
可持久化线段树
可持久化数据结构 (Persistent data structure) 是可以保留每一个历史版本,并且支持操作的不可变特性 (immutable)。
可持久化线段树即保留多个历史版本(第k个版本即第k次修改后,第k+1次版本修改前的版本)的线段树,支持对历史版本的访问和修改。
关于空间的讨论
建一个线段树的空间是O(N)的,如果有M次修改就需要O(M*N)的空间?这显然是不行的
(只会单点修改的可持久化线段树,所以下面提的修改,均为单点修改。)
若熟悉线段树应该不难发现,一次单点修改只会影响被修改的叶子结点到根这条路径上的结点,所以被影响的结点个数显然是 O ( l o g N ) O(logN) O(logN)的。
举个栗子
原序列为:A[1000,200,30,4]。
这是一颗维护区间和的线段树。
现在要,先将A[3]修改成50,然后将A[2]修改成600。过程如下。
发现每次确实只修改了上个版本logn个点的信息。于是乎,为了节省空间,我们每次新建一颗树,可以依托于上一个版本的线段树,如下。
第一次操作:把第 3 个位置改为 50。我们只要新建 log n 个新点,剩下的结点与第0版本共用。
得到第一版本线段树。
第二次操作:把第二个位置改成50,同样新建logn个点,其余与第1。
得到第二版本线段树。
完整视图。
由这个例子可以发现,每次修改操作都会增加 logn个点,所以空间复杂度变成了 nlog,一般开32倍的数组就行了
时间复杂度依旧是 O(mlogn)。
用数组实现的时候不能用 2x和 2x+1表示左右孩子结点了,所以每个结点需要增加Ls和Rs两个指针。
例题
模板题洛谷P3919
题目描述
如题,你需要维护这样的一个长度为 NN 的数组,支持如下几种操作
在某个历史版本上修改某一个位置上的值
访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int a[maxn];
struct tree
{
int ls,rs,val;//左右孩子编号,
}node[maxn<<5];
int root[maxn];//每棵线段树根节点编号
int node_cnt;
void build(int &idx,int l,int r)
{
idx=node_cnt++;
if(l==r)
{
node[idx].val=a[l];
return;
}
int mid=l+r>>1;
build(node[idx].ls,l,mid);
build(node[idx].rs,mid+1,r);
}
//依托的版本和新版本线段树编号,当前结点左右边界,待修改位置和值
void modify(int pre,int &now,int l,int r,const int &loc,const int &val)
{
now=node_cnt++;
node[now]=node[pre];
if(l==r)
{
node[now].val=val;
return;
}
int mid=l+r>>1;
if(loc<=mid)
modify(node[pre].ls,node[now].ls,l,mid,loc,val);
else
modify(node[pre].rs,node[now].rs,mid+1,r,loc,val);
}
//
int query(int now,int l,int r,const int loc)
{
if(l==r)
return node[now].val;
int mid=l+r>>1;
if(loc<=mid)
return query(node[now].ls,l,mid,loc);
else
return query(node[now].rs,mid+1,r,loc);
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
for (int i = 1; i <= n; i++)
cin>>a[i];
build(root[0],1,n);
for(int i=1;i<=m;i++)
{
int v,opt,loc,val;
cin>>v>>opt;
if(opt==1)//修改
{
cin>>loc>>val;
modify(root[v],root[i],1,n,loc,val);
}
else{
cin>>loc;
int ans=query(root[v],1,n,loc);
root[i]=root[v];
cout<<ans<<'\n';
}
}
return 0;
}
权值线段树
个人感觉叫值域线段树更符合。
每个结点存储的信息为,当前值域的数的个数。如图所示:
但如果数字值域很大的话,例如[0,1e9],这样建树显然是不行的。而事实上,我们只关心数的大小关系就可。因此,我们需要也可以进行离散化。
以序列{245, 112, 45322, 98988}为例,把序列离散化为{2, 1, 4, 3},得到值域线段树如下:
若想要在值域线段树上查询第k小,我们只需要拿k和当前结点的左孩子权值(设为val[lson])和k比较,
如果k<=val[lson],去左子树找第k大的数,
否则去右子树找第k-val[lson]大的数。
主席树
将权值线段树进行可持久化就得到主席树啦。
具体是把每个序列区间[1,i]分别建一棵权值线段树,并称其为第
i
i
i版本线段树。如下图所示:
怎样查询序列中区间[L, R]的第k小。
如果能得到区间[L, R]的线段树,就能高效率地查询出第k小。根据前缀和的思想,区间[L, R]包含的元素等于区间[ 1 , R ] 减去区间[ 1 , L − 1 ]。把前缀和思想用于线段树的减法,线段树的减法,是在两棵结构完全的树上,把所有对应结点的权值相减。线段树R减去线段树L − 1 ,就得到了区间[ L , R ] 的线段树。
例如区间[2, 4]的线段树,等于把第4个线段树与第1个线段树相减(对应圆圈内的数字相减),得到下图的线段树:
但事实上,我们并不需要真的将两个线段树相减得到一棵新的树,只需要同时访问两个线段即可。类似于上述的可持久化线段树中的修改操作,具体见代码实现。
洛谷P3834
参考代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int num[maxn], num2[maxn];
int root[maxn];
struct node
{
int l, r, val; //左右孩子下标,值域内数字个数
} Node[maxn << 5];
int tot, node_cnt;
//前一个版本,当前版本,当前结点表示值域,要插入的值
void modify(int pre, int &now, int L, int R, const int &id)
{
now = ++node_cnt;
Node[now] = Node[pre];
Node[now].val++;
if (L == R)
return;
int mid = (L + R) >> 1;
if (id <= mid)
modify(Node[pre].l, Node[now].l, L, mid, id);
else
modify(Node[pre].r, Node[now].r, mid + 1, R, id);
}
//第l-1和第r个版本编号对应结点编号,当前结点表示值域,要查询的数
int query(int lx, int rx, int L, int R, int k)
{
int mid = (L + R) >> 1;
int sub = Node[Node[rx].l].val - Node[Node[lx].l].val;//获得当前序列区间对应线段树的值
if (L == R)
return L;
if (k <= sub)
return query(Node[lx].l, Node[rx].l, L, mid, k);
return query(Node[lx].r, Node[rx].r, mid + 1, R, k - sub);
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d", num + i), num2[i] = num[i];
//离散化
sort(num2, num2 + n + 1);
tot = unique(num2 + 1, num2 + n + 1) - num2 - 1;
for (int i = 1; i <= n; i++)
{
int id = lower_bound(num2 + 1, num2 + tot + 1, num[i]) - num2;
modify(root[i - 1], root[i], 1, tot, id);
//cout<<id<<endl;
}
while (m--)
{
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
int loc = query(root[l - 1], root[r], 1, tot, k);
printf("%d\n", num2[loc]);
}
return 0;
}