由于在某场令人伤心的比赛中知道了要用主席树的模板结果板子里没有主席树而错失银牌, 故下定决心好好学习主席树,争当手撸主席树一百分选手!
参考链接:点击打开链接
首先,主席树又叫可持久化线段树,是在线段树的基础上加上某些操作得到的!它可以得到历史版本的操作结果!例如,我用某棵线段树求解逆序数,然后问你1 ~ R区间的逆序数是多少?一般情况下的线段树是没有办法得到这种答案的(例如先问区间1 - 5, 再问区间1 - 3)当然你可以对于每一个右区间建立一个线段树,然而考虑到空间的问题,MLE是妥妥的!
现在考虑,如何在有限空间内实现以上功能。我们发现,第i - 1棵线段树和第i棵线段树只有叶子节点(假设为a),以及a的父节点,a的父节点的父节点,。。。根节点不相同(logn个)故我们只需要在第i - 1棵线段树的基础上加入logn个新节点即可生成第i棵线段树!最后的空间复杂度为nlogn。因为这种方法生成的线段树不是完全二叉树,故父节点和子节点就不再满足两倍和两倍+1的关系,需要记录左子节点和右子节点的下标!
代码如下:
①:生成空树
int bulid(int l, int r){
int rt = (++tot);
sum[rt] = 0;
if(l < r){
int m = (l + r) >> 1;
L[rt] = bulid(lson);
R[rt] = bulid(rson);
}
return rt;
}
②:往树上加点
int update(int pre, int l, int r, int x){
int rt = (++tot);
L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + 1;
if(l < r){
int m = (l + r) >> 1;
if(x <= m)
L[rt] = update(L[pre], lson, x);
else
R[rt] = update(R[pre], rson, x);
}
return rt;
}
③:查询操作
int query(int u, int v, int l, int r, int k){
if(l >= r)
return l;
int m = (l + r) >> 1, res = sum[L[v]] - sum[L[u]];
if(res >= k)
return query(L[u], L[v], lson, k);
else
return query(R[u], R[v], rson, k - res);
}
④:完整代码(hdu2665,查区间第k小)
#include <stdio.h>
#include <iostream>
#include <string>
#include <queue>
#include <map>
#include <vector>
#include <algorithm>
#include <string.h>
#include <cmath>
#define lson l, m
#define rson m + 1, r
using namespace std;
const int maxn = 1e5 + 10;
int a[maxn], T[maxn], ha[maxn], L[maxn * 40], R[maxn * 40], sum[maxn * 40], tot;
int bulid(int l, int r){
int rt = (++tot);
sum[rt] = 0;
if(l < r){
int m = (l + r) >> 1;
L[rt] = bulid(lson);
R[rt] = bulid(rson);
}
return rt;
}
int update(int pre, int l, int r, int x){
int rt = (++tot);
L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + 1;
if(l < r){
int m = (l + r) >> 1;
if(x <= m)
L[rt] = update(L[pre], lson, x);
else
R[rt] = update(R[pre], rson, x);
}
return rt;
}
int query(int u, int v, int l, int r, int k){
if(l >= r)
return l;
int m = (l + r) >> 1, res = sum[L[v]] - sum[L[u]];
if(res >= k)
return query(L[u], L[v], lson, k);
else
return query(R[u], R[v], rson, k - res);
}
int main(){
int n, m, t;
while(scanf("%d %d", &n, &m) != EOF){
tot = 0;
for(int i = 1; i <= n; i++){
scanf("%d", &a[i]);
ha[i] = a[i];
}
sort(ha + 1, ha + 1 + n);
int d = unique(ha + 1, ha + 1 + n) - ha - 1;
T[0] = bulid(1, d);
for(int i = 1; i <= n; i++)
T[i] = update(T[i - 1], 1, d, lower_bound(ha + 1, ha + 1 + d, a[i]) - ha);
for(int i = 1; i <= m; i++){
int l, r, k;
scanf("%d %d %d", &l, &r, &k);
printf("%d\n", ha[query(T[l - 1], T[r], 1, d, k)]);
}
}
return 0;
}
当然主席树也可以用来得到区间第k大问题。
我们观察主席树和线段树,发现主席树解决了[L, R]的区间询问,而原线段树只能查找[1, R]的询问。其利用了线段树的可加减的性质,即如果一个数在[1, R]是第k大, 在[1, l - 1]是第m大, 则它在[l, R]是第k - m大!
同时主席树也不仅仅能求解区间最值问题,例如求解区间逆序数数量之类~深入理解+灵活操作才能不虚任何变化!