關於主席樹
按老師說的,他第一次見到可持久化數據結構的時候,覺得它很神奇(其實只是沒見過世面而已)。
主席樹,這個名字是怎么來的呢?
原因,學長是這樣說的:因為發明這種數據結構的大佬名字縮寫和hjt主席一樣,於是,便叫主席樹。
下面進入正文:
主席樹,又稱函數式線段樹、可持久化線段樹。
傳說是一位大神沒學會划分樹,於是發明了這個數據結構,作為替代品。
想要學會主席樹,首先你要會線段樹,前綴和思想。
既然你點開了這篇博客,那你肯定是會線段樹的,不會我也沒辦法,下次寫,至於為什么先寫主席樹再寫線段樹,,嗯,,一時興起。
前綴和思想,這比較基礎了吧,如果你不會,那你白學到這里了。
首先我們看一道題目:
給出100000個數,向你提5000問,L到R間的第K大數是多少?
我們首先想到的那肯定是暴力啊!
但是一看數據范圍,肯定不行啊。
線段樹?
k值是不變的,你怎么寫??
滑動窗口+單調隊列?
如果是上面一種情況,l-x到l最大的三個值是1.2.3,l-x到r最大的三個值是4.5.6我們可以清晰地知道l到r之間最大的三個值是4.5.6.
單如果把4改成3呢?
你還能確定3是l到r里的?
那么接下來,我們需要我們親愛的主席樹了。
要知道第k大的值就要維護k個值,然后用前綴和的思想。
but剛剛就說了要線段樹,我們先看線段樹。
建立n+1棵線段樹,統計每個數到sz[i]時的出現次數。那么線段樹R比線段樹L-1多出來的數中的第K大就是答案。
那么問題來了: 數字如果太大,線段樹是裝不下的(因為求和啊)
解決的方法: 把所有的數排序、去重,線段樹中存儲的不是數本身而是數字的排序。
就是給他們一些編號,而且還快呢。
然后,空間,n+1棵線段樹。一棵線段樹需要開到n*4,那這不是要開到(n+1)n*4????
這顯然是不客觀的。
那么!我們主席樹的神奇之處就體現出來了!
我們可以用公用節點。
建立的過程:
1、數組排序、去重
sz: 2 4 6 8 8 6 4 2
hash:2 4 6 8
用hash的值對sz內的值進行更改變為
sz: 1 2 3 4 4 3 2 1
代碼:
for(int i = 1; i <= n; ++i) { readint(sz[i]); hash[i] = sz[i]; } sort(hash + 1, hash + n + 1); int siz = unique(hash + 1, hash + n + 1) - hash - 1; for(int i = 1; i <= n; ++i) sz[i] = lower_bound(hash + 1, hash + siz + 1, sz[i]) - hash;
2、建立root[0]
依照排序去重后的點數建立線段樹
[x,y]表示x到y的區間。
代碼:
build(root[0], 1, siz);
void build(node * &cur, int l, int r){ cur = (node *)malloc(sizeof(node)); cur -> cnt = 0; cur -> ch[0] = cur -> ch[1] = NULL; cur -> l = l; cur -> r = r; if(l != r){ int mid = (l + r) / 2; build(cur -> ch[0], l, mid); build(cur -> ch[1], mid + 1, r); } }
3、建立root[1]~root[n](依照數列的大小建立)
代碼:
void update(node * pre, int ps, node * &cur, int l, int r){ cur = (node *)malloc(sizeof(node)); cur -> cnt = pre -> cnt + 1; cur -> l = pre -> l; cur -> r = pre -> r; cur -> ch[0] = pre -> ch[0]; cur -> ch[1] = pre -> ch[1]; if(l == r) return ; int mid = (l + r) / 2; if(ps <= mid) update(pre -> ch[0], ps, cur -> ch[0], l, mid); else update(pre -> ch[1], ps, cur -> ch[1], mid + 1, r); }
for(int i = 1; i <= n; ++i) update(root[i - 1], sz[i], root[i], 1, siz);
4、查詢
以l,r,k分別為3、8、3為例
root[8]的左孩子-root[2]左孩子為2,說明第3大在右孩子中,查詢右孩子的k-2=1;
root[8]的右孩子的左孩子比root[2]的右孩子的左孩子大2,所以在root的右孩子的左孩子中,而此時已經到葉子,返回當前葉子的序號3。
所以3到8的第3大孩子為hash[3]=6。
代碼:
int query(node * lt, node *rt, int l, int r, int k) { if(l == r) return l; int mid = (l + r) / 2, cha = rt -> ch[0] -> cnt - lt -> ch[0] -> cnt; if(k <= cha) return query(lt -> ch[0], rt -> ch[0], l, mid, k); else return query(lt -> ch[1], rt -> ch[1], mid + 1, r, k - cha); }
5、節省空間的實際建圖
提醒一下:
空間為n*4+n*logn 不過要用數組模擬指針,不然你一遍遍用指針開空間,會MLE。
以下為數組模擬代碼:
#include <cstdio> #include <cstring> #include <iostream>
#include <algorithm> #define MAXN 100001 using namespace std; int n, m, root[MAXN], cut, a[MAXN], s[MAXN], t, x, y, z; struct data { int lc, rc, ans; } tree[MAXN * 20]; void add(int &now, int last, int l, int r, int x) { now = ++cut; tree[now].ans = tree[last].ans + 1; tree[now].lc = tree[last].lc, tree[now].rc = tree[last].rc; if(l == r) return ; int mid = (l + r) >> 1; if(x <= mid) add(tree[now].lc, tree[last].lc, l, mid, x); else add(tree[now].rc, tree[last].rc, mid + 1, r, x); return ; } int query(int L, int R, int l, int r, int x) { if(l == r) return l; int p = tree[tree[R].lc].ans - tree[tree[L].lc].ans; int mid = (l + r) >> 1; if(p >= x) return query(tree[L].lc, tree[R].lc, l, mid, x); else return query(tree[L].rc, tree[R].rc, mid + 1, r, x - p); } int main() { while(scanf("%d%d", &n, &m) != EOF) { cin >> n >> m; cut = 0; memset(root, 0, sizeof root); for(int i = 1; i <= n; i++) cin >> s[i] >> s[i]; sort(s + 1, s + n + 1); for(int i = 1; i <= n; i++) { int p = lower_bound(s + 1, s + n + 1, a[i]) - s; add(root[i], root[i - 1], 1, n, p); } while(m--) { cin >> x >> y >> z; int p = query(root[x - 1], root[y], 1, n, z); printf("%d\n", s[p]); } } return 0; }
有個簡單的問題,什么是愛情?