1. 程式人生 > 其它 >筆記:左偏樹

筆記:左偏樹

摘要:賀了三道題,啥也沒學會。

賀的是:hsfzLZH1 和 05年集訓隊論文 和 OI-wiki

左偏樹的定義和性質

我們定義一個 外節點 的 \(dis\) 是 1。每個點的 \(dis\) 等於它到最近的外節點的距離。特別地,空節點的 \(dis\) 是 -1。

那麼,對於一個 \(n\) 個節點的二叉樹,根的 \(dis\) 不超過 \(\log {(n+1)}-1\)

Proof

一個根節點 \(dis\)\(x\) 的二叉樹,他至少有 \(x\) 層是滿的。

\(n\ge 2^{x+1}-1\)

\(x\le \log {(n+1)}-1\)


左偏樹,是一棵滿足如下兩個性質的二叉樹。

兩個性質:

  • 具有左偏性質。即:\(\forall u\in T,dis(lson(u))\ge dis(rson(u))\)
  • 具有堆性質。

兩個結論:

  • \(dis(u)=dis(rson(u))+1\)。下圖是論文裡的示例圖。
  • 往右一直走,最多走 \(\log {(n+1)}-1\)
    原因:對於一個 \(n\) 個節點的二叉樹,根的 \(dis\) 不超過 \(\log {(n+1)}-1\)

左偏樹的操作

主要是合併,插入,刪除根,建樹,刪除任意節點。
這裡以小根堆為例。

合併

合併兩棵左偏樹。
把權值較大的左偏樹 \(B\) 併到 根權值較小的的左偏樹 \(A\) 的右子樹 \(right(A)\)

上,一層層並下去。

合併完了,這時候可能不再滿足 \(dis(left(A))\ge dis(right(A))\),這時就交換 \(A\) 的左右子樹。
(下圖中第一次交換是為了保證 \(A\) 的根 比 \(B\) 的根 權值小)

時間複雜度:每次向右走一步。\(O(\log n)\)

插入

直接將單點當做一棵樹,執行合併操作。\(O(\log n)\)

刪除根

合併倆兒子。\(O(\log n)\)

建樹

下面都是論文的解釋。

刪除任意節點

下面都是OI wiki。
先將左右兒子合併,然後自底向上更新 、不滿足左偏性質時交換左右兒子,當 \(dist\) 無需更新時結束遞迴:

int& rs(int x) { return t[x].ch[t[t[x].ch[1]].d < t[t[x].ch[0]].d]; }
// 有了 pushup,直接 merge 左右兒子就實現了刪除節點並保持左偏性質
int merge(int x, int y) {
  if (!x || !y) return x | y;
  if (t[x].val < t[y].val) swap(x, y);
  t[rs(x) = merge(rs(x), y)].fa = x;
  pushup(x);
  return x;
}

void pushup(int x) {
  if (!x) return;
  if (t[x].d != t[rs(x)].d + 1) {
    t[x].d = t[rs(x)].d + 1;
    pushup(t[x].fa);
  }
}

習題

  • P3377 【模板】左偏樹(可並堆)

    這真的適合當板題嗎。。逐漸不會並查集的路徑壓縮。

    code
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define fi first
    #define se second
    #define mkp make_pair
    #define PII pair<int,int>
    const int N = 1e5 + 10;
    int n, m, f[N], val[N], dis[N], tr[N][2];
    int find(int x) { return (x == f[x]) ? x : f[x] = find(f[x]); }
    int merge(int x, int y) {
        if(!x || !y) return x | y;
        if(val[x] > val[y] || (val[x] == val[y] && x > y)) swap(x, y); 
        tr[x][1] = merge(tr[x][1], y);
        if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]);
        f[tr[x][0]] = f[tr[x][1]] = x; //最多也是跳log次。 
        dis[x] = dis[tr[x][1]] + 1;
        return x;
    }
    void del(int x) {
        val[x] = -1;
        f[tr[x][0]] = tr[x][0]; f[tr[x][1]] = tr[x][1];
        f[x] = merge(tr[x][0], tr[x][1]); //子樹中有些點路徑壓縮到的還是x,讓他們跳到正確的根。 
    }
    int main() {
        dis[0] = -1;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++)
            scanf("%d", &val[i]), f[i] = i;
        while(m--) {
            int op, x, y;
            scanf("%d", &op);
            if(op == 1) {
                scanf("%d%d", &x, &y);
                if(val[x] == -1 || val[y] == -1) continue;
                x = find(x); y = find(y);
                if(x != y) f[x] = f[y] = merge(x, y);
            } else {
                scanf("%d", &x);
                if(val[x] == -1) { puts("-1"); continue; }
                int y = find(x); printf("%d\n", val[y]); del(y);
            }
        }
        return 0;
    }
    
  • [APIO2012]派遣

    是誰在賀自己以前的程式碼?是我啊,那沒事了。

    題意:給你一棵樹(保證 \(fa_i<i\)),每個節點有val,lead。對於一個點,在它子樹裡選 \(\sum val \le m\) 的點,\(ans=max(ans,lead*tot)\) 。求最大ans

    題解:

    每個點維護一個堆,表示當前它子樹中價效比最高且 \(\sum val \le m\) 的點。

    從下到上遍歷樹(這裡就直接從 \(n\) 掃到 \(1\)) 每次把該點的子樹和它父親當前子樹合併。

    每個點和父親合併一次,被刪除至多一次。\(O(\log n)\)

    code
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define fi first
    #define se second
    #define mkp make_pair
    #define PII pair<int,int>
    const int N = 1e5 + 10;
    int n, m, fa[N], tot[N], val[N], lead[N], dis[N], tr[N][2], rt[N];
    ll sum[N];
    int merge(int x, int y) {
        if(!x || !y) return x | y;
        if(val[x] < val[y]) swap(x, y); 
        tr[x][1] = merge(tr[x][1], y);
        if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]);
        dis[x] = dis[tr[x][1]] + 1;
        return x;
    }
    int main() {
        dis[0] = -1;
        scanf("%d%d", &n, &m);
        ll ans = 0;
        for(int i = 1; i <= n; i++) {
            scanf("%d%d%d", &fa[i], &val[i], &lead[i]);
            rt[i] = i; sum[i] = val[i]; tot[i] = 1;
            ans = max(ans, 1ll * lead[i]);
        }
        for(int i = n; i > 1; i--) {
            sum[fa[i]] += sum[i]; tot[fa[i]] += tot[i];
            rt[fa[i]] = merge(rt[fa[i]], rt[i]);
            while(sum[fa[i]] > m) {
                sum[fa[i]] -= val[rt[fa[i]]]; tot[fa[i]]--;
                rt[fa[i]] = merge(tr[rt[fa[i]]][0], tr[rt[fa[i]]][1]);
            }
            ans = max(ans, 1ll * lead[fa[i]] * tot[fa[i]]);
        }
        printf("%lld\n", ans);
        return 0;
    }
    
  • [BalticOI 2004]Sequence 數字序列

    論文題/yun

    首先考慮將嚴格遞增改成單調不降,\(b[i]-i\) 是單調不降序列。

    不妨以 \(a[i]-i\) 作為 \(a\) 求單調不降的 \(b\),最後答案每個 \(+i\)

    考慮特殊情況:

    • 情況1:\(a\) 單調不降:\(a=b\)
    • 情況2:\(a\) 單調不增:\(b_i\) 都是 \(a\) 中位數

    考慮將原序列分成 \(m\) 段,每一段都是 情況2。(情況1 可以分裂成很多 情況2)
    我們希望 \(b\) 遞增,如果有相鄰兩段不遞增,那麼這一整段我們都取原來 \(a\) 數組裡這一整段的中位數。

    Proof

    \(a_{1}\)\(a_n\) 中位數 為 \(u\)\(a_{n+1}\)\(a_m\) 中位數是 \(v\)\(a_{1}\)\(a_m\) 中位數是 \(w\)

    現在的 \(b\)\((u,u,...,u,v,v,...v)\)

    如果 \(u\le v\),那很顯然不用動了
    如果 \(u>v\),我們要證明不存在答案比 \((w,w,w...w)\) 更優的 \(b\)
    /ll 看論文吧。

    現在問題變成了:合併兩個有序集以及查詢某個有序集內的中位數。

    我們發現,只有當某一區間內的中位數比後一區間內的中位數大時,合併操作才會發生,也就是說,任一區間與後面的區間合併後,該區間內的中位數不會變大。

    於是我們可以用最大堆來維護每個區間內的中位數,當堆中的元素大於該區間內元素的一半時,刪除堆頂元素,這樣堆中的元素始終為區間內較小的一半元素,堆頂元素即為該區間內的中位數。

    ——論文

    我們維護的堆,是前 \(\lceil \frac {len} 2\rceil\) 大的值。
    使用左偏樹,查詢 \(O(1)\) ,合併 \(O(\log n)\)。 可以達到 \(O(n\log n)\) 的複雜度。

    code
    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define fi first
    #define se second
    #define mkp make_pair
    #define PII pair<int,int>
    const int N = 1e6 + 10;
    int n, m, dis[N], tr[N][2], val[N], a[N], b[N];
    int top;
    struct node {
        int rt, l, r, sz, mid;
        node() {
            rt = 0; l = 0; r = 0; sz = 0; mid = 0;
        }
        node(int smrt, int sml, int smr, int smsz, int smmid) {
            rt = smrt; l = sml; r = smr; sz = smsz; mid = smmid;
        }
    }st[N];
    int merge(int x, int y) {
        if(!x || !y) return x | y;
        if(val[x] < val[y]) swap(x, y); 
        tr[x][1] = merge(tr[x][1], y);
        if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]);
        dis[x] = dis[tr[x][1]] + 1;
        return x;
    }
    int main() {
        dis[0] = -1;
        scanf("%d", &n);
        for(int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
            val[i] = a[i] - i;
        }
        for(int i = 1; i <= n; i++) {
            st[++top] = node(i, i, i, 1, a[i] - i);
            while(top > 1 && st[top - 1].mid > st[top].mid) {
                top--;
                st[top].r = st[top + 1].r;
                st[top].sz += st[top + 1].sz;
                st[top].rt = merge(st[top].rt, st[top + 1].rt);
                while(st[top].sz > (st[top].r - st[top].l + 2) / 2) {
                    st[top].sz--;
                    st[top].rt = merge(tr[st[top].rt][0], tr[st[top].rt][1]);
                }
                st[top].mid = val[st[top].rt];
            }
        }
        for(int i = 1; i <= top; i++)
            for(int j = st[i].l; j <= st[i].r; j++)
                b[j] = st[i].mid + j;
        ll ans = 0;
        for(int i = 1; i <= n; i++) ans += abs(b[i] - a[i]);
        printf("%lld\n", ans);
        for(int i = 1; i <= n; i++) printf("%d ", b[i]); puts("");
        return 0;
    }
    /*
    5
    2 5 46 12 1
    */