主席树知识整理
可持久化线段树,别名主席树。
在我看来这个数据结构绝妙之处在于它把可持久化的概念用于解决区间查询的问题。
一般主席树用于解决区间第k大的问题。
我们建立一棵权值线段树,权值线段树就是线段树上第i个叶子节点储存i出现了多少次(类似于桶排序)
假想,一个数列第i个元素a[i]=p,是在第i个时间节点插入线段树的,根据可持久化数据结构的概念,每一个时间节点被更新的部分都建立新的节点。对于每个时间节点,根节点肯定被更新过,因此,一个序列中n个元素,在n个时间节点插入,就有n个根节点。
那么,如果要求[l,r]区间内的第k大,实际上就是把r时间节点的树根下的元素集合,和l-1时间节点树根下的元素集合做差,然后的过程类似于线段树上的二分查询。
具体方法是,我们把l-1和r两个时间节点的树根拿出来,看它们的左子树,设w=r时间节点的左子树上的元素数减去l-1时间节点的左子树的元素数,如果w>=k,那么就递归查询左子树上第k大的,否则查询右子树上第k-w大的。
主席树能够解决什么问题取决于线段树上保存的信息是什么,但是一般主席树的用法都比较死板,除了查询区间第k大,常见的用法只有查询区间比k小的元素有多少个,具体的方法就是分别查询两个时间节点上,根节点1~k的前缀和,然后做差。
#include <bits/stdc++.h> #define LL long long using namespace std; const int MAXN = 1e5 + 10; struct node { int ls, rs, sum; //左子树,右子树,该节点值 } ns[MAXN * 20]; int ct; //时间节点 int rt[MAXN * 20]; //rt[i]代表a[i]在树上是第几个添加的 void cpy(int& now, int old) { now = ++ct; ns[now] = ns[old]; } void pushUp(int& now) { ns[now].sum = ns[ns[now].ls].sum + ns[ns[now].rs].sum; } void build(int& now, int l, int r) { now = ++ct; ns[now].sum = 0; if (l == r) return; int m = (l + r) >> 1; build(ns[now].ls, l, m); build(ns[now].rs, m + 1, r); } void update(int& now, int old, int l, int r, int x) { cpy(now, old); //在旧树上添加新树 if (l == r) { ns[now].sum++; return; } int m = (l + r) >> 1; if (x <= m) update(ns[now].ls, ns[old].ls, l, m, x); else update(ns[now].rs, ns[old].rs, m + 1, r, x); pushUp(now); //向上更新节点权值 } int query(int s, int t, int l, int r, int k) { if (l == r) return l; int m = (l + r) >> 1; int cnt = ns[ns[t].ls].sum - ns[ns[s].ls].sum; //cout << s << " " << t << " " << cnt << endl; if (k <= cnt) return query(ns[s].ls, ns[t].ls, l, m, k); return query(ns[s].rs, ns[t].rs, m + 1, r, k - cnt); // } void init(int n) { ct = 0; build(rt[0], 1, n); //从头开始建立主席树 }