1. 程式人生 > >「學習筆記」可持久化線段樹

「學習筆記」可持久化線段樹

中位數 root lca peak 繼續 bzoj2653 進行 turn size

註:此博客寫於 2017.12

可持久化線段樹

常見的一個實現是主席樹,由HJT主席引入中國OI界。

基本思想

考慮一顆不同的線段樹,進行單點修改。註意到每一次只會修改 \(\log\) 個節點。

引入函數式編程的思想,我們不進行修改,而是新建節點。操作的時間復雜度仍然為 \(O(log n)\),但是空間為 \(O(n log n)\)

從第 \(i\) 個根開始訪問即可得到第 \(i\) 次修改後的歷史版本。

經典應用:區間K小值。

考慮建立權值線段樹,按照下標插入,\([l,r]\) 中,某個元素出現了多少遍,可以通過 \(r\)\(l-1\) 版本的線段樹作差得到。然後在主席樹上二分即可。

一些好題

BZOJ1146-[CTSC2008]網絡管理Network 維護一棵樹,要求支持:單點修改權值,路徑第 \(K\) 大。
\(n, Q \leq 80000\)

樹上帶修改主席樹,真·裸題。 首先這道題從2個log到4個log的算法都能過。話說zzd Dalao用了二分答案+樹鏈剖分+主席樹,竟然只比兩個log慢4倍!

樹上主席樹有一個常見的套路:每一個節點的版本,都是在父節點的基礎上修改。這樣 \(root[x] + root[y] - root[lca(x,y)] - root[fa[lca(x,y)]]\) 就得到這條鏈了。

再考慮這個帶修改。註意到,一個點修改之後,只對它的子節點與外界的詢問有影響。稍微分類討論一下,就能發現,我們只需要修改子樹中所有版本即可。

這個可以用樹狀數組套權值線段樹解決。修改的時候,需要改變 \(\log\) 個節點;查詢的時候,也只需要考慮 \(\log\) 個節點的貢獻總和。

復雜度 \(O(n \log ^ 2 n)\)

BZOJ3674-可持久化並查集加強版 維護可持久化並查集。

可持久化數組。 註意到主席樹就是一個可持久化數組,維護並查集的 \(fa,rank\) ,按秩合並即可。

BZOJ2653-middle 給定一個長度為 \(n\) 的序列,和若幹個詢問。每個詢問 \((a,b,c,d)\) 要求求出 \(l\)\([a,b]\) 之間, \(r\)\([c,d]\) 之間的,構成的子序列的中位數的最大值。

二分答案,主席樹,中位數。 中位數嘛,有一個常見的套路:二分答案 \(x\) ,令所有小於 \(x\)\(-1\),否則為 \(1\) 。如果左後所有數之和大於等於 \(0\) ,說明符合要求,可以繼續變大。

這題同樣也是二分答案。註意到,對於區間 \([b,c]\) 是一定要選擇的。而對於 \([a,b-1]\)\([c+1,d]\) 我們最好是選擇一個最大的前綴和後綴。就是線段樹最基礎的維護了。

預處理主席樹的時候,默認所有數都為 \(1\) ,插入一個數之後,修改對應的位置為 \(-1\) ,這樣就能方便判斷了。

BZOJ3545-[ONTAK2010]Peaks 給定一張圖,每個點有點權,邊有邊權。查詢 \((v,x,k)\) 要求回答:從 \(x\) 出發,走邊權不超過 \(x\) 的邊,能都到達的第 \(k\) 大點權。

並查集虛擬點,dfs序,主席樹。 顯然,根據最小瓶頸樹的理論,求出的最小生成樹一定是最優的。

考慮按照邊權從小到大插入。對於邊 \((u,v)\) ,我們找到它們對應的祖先 \(a,b\) 造一個虛擬點 \(c\) 對應的點權是 \((u,v)\) 的邊權。

這樣有什麽用處呢?發現對於點 \((x,y)\) ,他們的 \(lca\) 對應的點權,就是從 \(x\)\(y\) 需要經過路徑最大值的最小值。

這樣就可以搞出整棵,包含 \(2n-1\) 個節點的樹了。首先倍增預處理,對於 \(v\) ,我們倍增向上跳到點 \(u\),但是點權不能超過 \(x\) ,那麽, \(v\) 能達到的點就是 \(u\) 的子樹!

dfs序預處理一下,然後就是區間K大了。

BZOJ3772-精神汙染 給定一棵樹,和若幹條路徑,求其中一條路徑包含另一條路徑的概率。

歐拉序,主席樹。 確實是一道好題。註意到,這相當於求 一條路徑的兩個端點都在另一條路徑上的方案數。

考慮首先限制 \(x\) 。對於 \((x,y)\),每個點建一棵主席樹,\(x\) 在父親版本的基礎上加入另一個端點 \(y\)

對於查詢路徑上兩個端點都被包含,相當於是路徑上插入的另一個點也在路徑上。

於是問題就轉化為 統計路徑上的點數之和。

可以用歐拉序:保存一個節點入棧和出棧的時間。入棧的位置 \(+1\) ,出棧的位置 \(-1\)

有一個很神奇的性質:\((x,y)\) 之間的點數之和(\(x,y\)是祖先/後代關系),就是 \([in[x],in[y]]\)之和(其他節點都沒有計算,或者被抵消;其實就是 \((x,y)\)當前還在棧中)。路徑就拆成 \([x,lca],[lca,y],[lca,lca]\)

主要代碼,

void insert(int &o, int l, int r, int x, int y);
int query(int o1, int o2, int o3, int o4, int l, int r, int x, int y) {
    if (l == x && y == r) return T[o1].sum + T[o2].sum - T[o3].sum - T[o4].sum;
  ...
}
void dfs1(int u) {
    in[u] = ++clk;
  ...
    out[u] = ++clk;
}
void dfs2(int u) {
    root[u] = root[fa[u][0]];
    loop (k, headV[u], linkV) {
        insert(root[u], 1, 2*n, in[v[k]], 1);
        insert(root[u], 1, 2*n, out[v[k]], -1);
    }
  ...
}
int main() {
    rep (i, 1, m) {
        a[i] = read(); b[i] = read();
        addEdge(headV, v, linkV, sizeV, a[i], b[i]);
    }
    dfs1(1);
  ...
    dfs2(1);
    rep (i, 1, m) {
        c = lca(a[i], b[i]);
        ans += query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[a[i]]);
        ans += query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[b[i]]);
        ans -= query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[c]) + 1;
    }
  ...
    return 0;
}

BZOJ4546-[CodeChef]XRQRS 給定一個初始時為空的整數序列以及一些詢問:
類型1:在數組後面就加入數字 \(x\)
類型2:在區間 \([L,R]\) 中找到 \(y\),最大化 \(x xor y\)
類型3:刪除數組最後 \(K\)個元素。
類型4:在區間 \([L,R]\) 中,統計小於等於 \(x\) 的元素個數。
類型5:在區間 \([L,R]\) 中,找到第 \(k\) 小的數。

可持久化Trie樹。 本質上就是主席樹,都是最基礎的操作。需要註意的是操作 \(2\) ,就是經典的 Trie樹 上貪心。

「學習筆記」可持久化線段樹