用一句話定義歸併樹就是用線段樹記錄歸併排序時每一層每一段數組的狀態。
用途:
- 歸併樹可以在 複雜度查找區間[l,r]比x小的有幾個.
- 那麼就可以通過二分枚舉在 複雜度實現查找區間[l,r]第k大的值.
我們先回顧一下歸併排序的具體實現方法:
- 歸併排序依靠遞歸的思想, 每次把當前要排序的區間分成儘可能相等的兩部分, 左右兩部分分別進行歸併排序, 之後再將排好序的左右兩個區間合併。 當區間長度爲1的時候, 停止遞歸。
複雜度分析:
因爲每次將當前序列分成兩部分遞歸, 最多分 層, 每層有n個元素, 所以歸併排序的複雜度是 .
歸併排序最常用的就是求逆序對的個數, 其實用樹狀數組也可以在相同的複雜度下求出逆序對。
c++實現代碼:
//參考紫書算法競賽入門經典
void merge_sort(int *A, int l, int r, int *T){ //[l, r) 排序. 外部調用區間爲[0, n)
if(r - l > 1){
int mid = l+(r-l)/2;
int p = l, q = mid, now = l;
// 對左右兩部分區間分別歸併排序
merge_sort(A, l, mid, T);
merge_sort(A, mid, r, T);
// 合併左右兩部分
while(p < mid || q < r){
if(q >= r || (p < mid && A[p] <= A[q])){
T[now++] = A[p++];
}
else{
T[now++] = A[q++];
cnt += mid - p; // cnt記錄的是逆序對個數
}
}
for(int i = l; i < r; i++) A[i] = T[i];
}
}
以上是對歸併排序的複習, 下面繼續學習歸併樹。
先說一下歸併樹的大致思想, 用vector維護出每一個節點的區間[l,r)排完序之後的數組, 每次查詢的時候, 分三種情況.
- 如果要查詢的區間和當前的區間沒有交集, 返回0個.
- 如果要查詢的區間完全包含了當前的區間, 用二分搜索對於當前節點保存的數組進行查找。
- 負責的話遞歸左右兒子查找並且求和。
根據歸併排序的思想, 要先初始化歸併樹, 原理同歸並排序, 只不過把數組的值都存起來了, 每次詢問的時候, 分上面說的三種情況就可以了。 具體看下面這一道例題以及模板代碼。
模板例題: 2104 - K-th Number
// 代碼參考白書(挑戰程序設計競賽)
const int MAXN = 1e5;
const int ST_SIZE = (1 << 18) - 1;
int n, m;
int a[MAXN+5], num[MAXN+5];
// num用來存a排好序之後的數組, 方便二分
vector<int> dat[ST_SIZE];
// 初始化歸併樹, 原理和歸併排序一樣, 區間[l, r)
void init(int k, int l, int r){
if(r - l == 1){
dat[k].push_back(a[l]);
}
else{
int lch = k * 2 + 1, rch = k * 2 + 2;
init(lch, l, (l+r)/2);
init(rch, (l+r)/2, r);
dat[k].resize(r-l);
merge(dat[lch].begin(), dat[lch].end(), dat[rch].begin(), dat[rch].end(), dat[k].begin());
// 利用STL自帶的merge函數把兩個兒子的數列合併
}
}
// 計算[ql, qr) 中不超過x的個數
// k是節點的編號, 對應區間[l, r)
int query(int ql, int qr, int x, int k, int l, int r){
if(qr <= l || r <= ql){//完全不相交
return 0;
}
else if(ql <= l && r <= qr){ // 詢問完全包含當前區間
return upper_bound(dat[k].begin(), dat[k].end(), x) - dat[k].begin();
}
else{
int lcnt = query(ql, qr, x, k*2+1, l, (l+r)/2);
int rcnt = query(ql, qr, x, k*2+2, (l+r)/2, r);
return lcnt + rcnt;
}
}