主席树
主席树是一种可持久化线段树、其发明者orz 黄嘉泰 拼音缩写与某届主席一样、于是这个数据结构被戏称为主席树。
所谓的“持久化数据结构”、就是保存这个数据结构的所有历史版本、同时利用它们之间的共用数据减少时间和空间的消耗。
由于线段树在区间长度固定的情况下结构都是一致的、主席树能够通过两颗线段树相减来得到某一区间的信息。
至于主席树的作用、其能够查询不修改的区间K大值、区间不同数的个数、套个树状数组还能查询动态K大值......
给出几篇文章以便学习 ==> 链接II、链接II、链接III
一些题目 :
HDU 2665 (可作为模板使用)
题意 : 给出一个整数序列、有若干个问询、每次问询 (L, R, K) 表示 L~R 区间内第 K 大的值是多少
分析 : 比较裸的主席树题目
首先先对于每个前缀按权值建出主席树、然后问询的时候就可以通过减法得到区间 (L, R) 的信息
由于存储的是值域信息、查询K大值的时候就判断左右子区间的元素个数与K的大小便能知道往哪个方向走
#include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; struct NODE{ int sum, L, R; NODE(){}; NODE(int _sum, int _L, int _R): sum(_sum),L(_L),R(_R){}; }T[maxn*18]; int Tcnt = 0; int root[maxn]; int arr[maxn], N; int uni[maxn], uniLen; int newNode(int sum, int L, int R) { T[++Tcnt] = NODE(sum, L, R); return Tcnt; } inline void Insert(int &root, int pre, int pos, int L, int R) { root = newNode(T[pre].sum+1, T[pre].L, T[pre].R); if(L == R) return ; int M = L + ((R-L)>>1); if(pos <= M) Insert(T[root].L, T[pre].L, pos, L, M); else Insert(T[root].R, T[pre].R, pos, M+1, R); } int Kth(int x, int y, int L, int R, int K) { if(L == R) return L; int M = L + ((R-L)>>1); int L_sum = T[T[y].L].sum - T[T[x].L].sum; if(K <= L_sum) Kth(T[x].L, T[y].L, L, M, K); else Kth(T[x].R, T[y].R, M+1, R, K - L_sum); } int main(void) { int nCase; scanf("%d", &nCase); while(nCase--){ T[0] = NODE(0, 0, 0);///将 0 号节点的左右子树指向自己 Tcnt = root[0] = 0;///便不用显式建树 int Q; scanf("%d %d", &N, &Q); for(int i=1; i<=N; i++){ scanf("%d", &arr[i]); uni[i-1] = arr[i]; } sort(uni, uni+N); uniLen = unique(uni, uni+N) - uni;///离散化 for(int i=1; i<=N; i++){ int pos = lower_bound(uni, uni+uniLen, arr[i]) - uni; pos++; Insert(root[i], root[i-1], pos, 1, uniLen+1); } int l, r, k; while(Q--){ scanf("%d %d %d", &l, &r, &k); int pos = Kth(root[l-1], root[r], 1, uniLen+1, k); printf("%d\n", uni[--pos]); } } return 0; }
题意 : 给出 n 个整数、每次问询一个区间 (L, R) 问这个区间内不同数的个数是多少?
分析 :
这题很久之前用线段树离线做过 ==> Click here
如果使用主席树的话就能做到在线回答问询
具体做法的核心思路其实和线段树离线的时候差不多
但是这里主席树存储的并不是值域信息、而是区间具体每个位置是否包含一种数
也就是这题主席树区间代表的信息和普通线段树所代表的信息一样
主席树每次按照前缀建树、建树的时候保证每一种数只保留最右边的位置
然后对于问询 (l, r) 只要在 root[r] 这颗树上查询端点 l 右边的所有 sum 值的和即可
#include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; struct NODE{ int sum, L, R; NODE(){}; NODE(int _sum, int _L, int _R): sum(_sum),L(_L),R(_R){}; }T[maxn*18]; int Tcnt = 0; int root[maxn]; int arr[maxn], N; int newNode(int sum, int L, int R) { T[++Tcnt] = NODE(sum, L, R); return Tcnt; } inline void Insert(int &root, int pre, int pos, int val, int L, int R) { root = newNode(T[pre].sum+val, T[pre].L, T[pre].R); if(L == R) return ; int M = L + ((R-L)>>1); if(pos <= M) Insert(T[root].L, T[pre].L, pos, val, L, M); else Insert(T[root].R, T[pre].R, pos, val, M+1, R); } int query(int rt, int pos, int L, int R) { if(L == R) return T[rt].sum; int ret = 0; int M = L + ((R-L)>>1); if(pos <= M) ret += query(T[rt].L, pos, L, M) + T[T[rt].R].sum;///递归进入左边区间的时候、要把右边区间的和加上 else ret += query(T[rt].R, pos, M+1, R); return ret; } map<int, int> mp; int main(void) { T[0] = NODE(0, 0, 0); root[0] = 0; Tcnt = 0; scanf("%d", &N); for(int i=1; i<=N; i++) scanf("%d", &arr[i]); for(int i=1; i<=N; i++){ if(mp.count(arr[i])){ int tmpRoot; Insert(tmpRoot, root[i-1], i, 1, 1, N); Insert(root[i], tmpRoot, mp[arr[i]], -1, 1, N); }else Insert(root[i], root[i-1], i, 1, 1, N); mp[arr[i]] = i; } int Q; scanf("%d", &Q); while(Q--){ int l, r; scanf("%d %d", &l, &r); printf("%d\n", query(root[r], l, 1, N)); } return 0; }