下笔春蚕食叶声。

笔记:左偏树

摘要:贺了三道题,啥也没学会。

贺的是: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
    */
    
posted @ 2022-04-03 16:01  ACwisher  阅读(72)  评论(1编辑  收藏  举报