1. 程式人生 > 實用技巧 >工廠方法模式

工廠方法模式

本文為作者原創,禁止轉載。

主席樹

本文講的是靜態主席樹,不涉及修改。

在瞭解主席樹之前,你需要先了解字首和與權值線段樹。這裡以HDOJ2665為例,來講解靜態主席樹。題目是給定區間,問區間第k大數。

首先對於一個區間,它的值域是一個集合,例如,給定陣列a[10] = {1, 73 ,73 ,5 ,22, 4, 6, 22, 81, 0},它的值域就是A={0,1, 4,5,6,22,73, 81},可以看到這裡涉及到去重和離散(離散就是把一串不連續的資料放在一個數組裡,這樣根據連續的下標來取值),對於一個已經離散化的陣列,根據下標就能知道他代表的值,就是i與a[i]的關係,a[i]是初值。對於上述陣列,我們想求1到73之間有多少個數,我們只需在離散後的陣列二分查詢到其下標,即求1到7(下標從1開始)之間有多少個。

知道了離散的概念後,我們根據這個離散陣列建一棵線段樹,代表主席樹的最初狀態。下面以[53,89,12,64]為例。

先建立一棵空樹:

此時一個點都沒插入,所有節點的值都為0。

下面就開始操作主席樹了。首先按照原始陣列的順序插入值,第一個是53,在離散後的陣列中可以發現他的下標是2。下面咱就再建一顆線段樹。

53在第二個下標處,所以[2,2]區間的數就+1,那麼[1,2]的數也+1,那麼[1,4]的數也+1,發現啥了沒有,[3,4]和[1,1]根本沒動。沒動我還騰著這個空間幹嘛?在之後每插入一個數,你都會發現很多節點都沒更新,我們來算算,在給定的長度為n的初始陣列,這麼建要多少空間。

首先,根據線段樹的結構你會發現,每一層的節點數都是2的冪,其中葉節點的數量不足2的冪也要補足。我們可以發現,葉節點的冪至多是

,就代表就k層,用個等比數列求和公式求一下不難算出節點數為個,帶入n就是個,後面的一略去,n顆線段樹就是個,這裡n最大為,則對最大空間數就是,這不MLE才怪。

考慮到上面有很多節點並沒有更新,沒更新代表這些點和之前的一顆線段樹的這些節點是一致的。於是我們就考慮將這些資訊共用,這樣我們插入一個數時,就讓第二顆線段樹的左右孩子等於之前一個線段樹的左右孩子的下標,哪個不一樣,我們再開一個空間給他,這樣空間利用率大大提升。我們發現,每次更新時,只更新了一條鏈,而鏈長就是層數,層數就是k+1,忽略常數,我們不難算出優化後的主席樹的空間複雜度為,帶入常數計算得最大節點數約為,空間複雜度大大減少。

下面來模擬插入的操作。

首先插入53,他在離散後的陣列下標為2,那麼[3, 4]、[1,1]就直接用了,不用再開了。

注意,綠色的這條鏈其實是它所對應根節點的左子樹。

再插入89

再插入12

最後插入64

不妨模擬一遍,你會發現so easy~

以上都是更新操作,查詢操作其實也很簡單,我們上面維護的其實是一個字首和,例如我們查詢[l, r]第k大數,我們先查詢[1, r]裡數的個數,再查詢[1, l-1]裡的個數,相減就是[l, r]區間裡的數,然後再在[l, r]裡進行查詢。

下面是AC程式碼

#include <bits/stdc++.h>
#define _for(i, a, b) for (int i = (a); i <= (b); ++i)
#define sc(a) scanf("%d", &a)
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int maxn = 1e5 + 5;
const int maxm = maxn * 20;
/*
各個陣列的用處:
val陣列用來儲存該節點的值,ls陣列用來儲存該節點的左孩子,rs陣列用來儲存右孩子,root陣列用來儲存根節點的下標
a陣列用來儲存原式值(輸入),b陣列用來去重,去重完a陣列就可以用作他用,儲存別的值
注意:開結構體會MLE
*/
int val[maxm], ls[maxm], rs[maxm], a[maxn], b[maxn], root[maxm], n, tot;
//建一棵空樹
void build(int &node, int l, int r) {
    node = ++tot;   //注意建樹順序
    val[node] = 0;  //空樹值為0
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(ls[node], l, mid);
    build(rs[node], mid + 1, r);
}
//插入值,每次插入則多加一條鏈,pre代表上次的根節點,pos代表插入的位置,node代表現在要更新的值
void update(int &node, int l, int r, int pre, int pos) {
    node = ++tot;   //每個節點都有對應的編號
    ls[node] = ls[pre]; //先讓該節點的左右節點等於上個等位節點的左右孩子
    rs[node] = rs[pre];
    //更新的節點要+1
    val[node] = val[pre] + 1;
    if (l == r) return;
    int mid = (l + r) >> 1;
    //觀察主席樹,理解裡面每個引數的意義,更新要更新的孩子
    if (pos <= mid) update(ls[node], l, mid, ls[pre], pos);
    else update(rs[node], mid + 1, r, rs[pre], pos);
}
int query(int st, int ed, int l, int r, int k) {
    if (l == r) return l;
    int mid = (l + r) >> 1;
    //利用字首和計算該區間內數的個數
    int cnt = val[ls[ed]] - val[ls[st]];
    //這裡不好解釋,模擬一遍就懂了
    if (k <= cnt) return query(ls[st], ls[ed], l, mid, k);
    else return query(rs[st], rs[ed], mid + 1, r, k - cnt);
}
signed main() {
    int kase; sc(kase);
    while (kase--) {
        int q; sc(n), sc(q);
        _for(i, 1, n) {
            sc(a[i]);
            b[i] = a[i];
        }

        //去重
        sort(b + 1, b + 1 + n);
        int num = unique(b + 1, b + 1 + n) - b - 1;

        tot = 0;
        build(root[0], 1, num);

        _for(i, 1, n) a[i] = lower_bound(b + 1, b + 1 + num, a[i]) - b; //找出這個數在離散後數組裡位置,直接用a陣列存
        _for(i, 1, n) update(root[i], 1, num, root[i - 1], a[i]);

        int x, y, z;
        while (q--) {
            sc(x), sc(y), sc(z);
            //找出該區間在離散後數組裡的位置
            int ans = query(root[x - 1], root[y], 1, num, z);
            printf("%d\n", b[ans]);
        }
    }
    return 0;
}