..

第八章 线段树的进阶用法

我们知道,对于一棵线段树而言,如果它的总长度不变,那么它的形态是不会改变的。也就是说,在值域不变情况下,权值树的形态是不会改变的。这样一来,就可以对权值树进行合并的操作。对于权值树 $A, B$,若 $A, B$ 形态相同,则可以合并这两棵权值树,合并的方式就是对应节点相加。

显然,合并后的树依然是一棵权值线段树。可以将权值线段树的合并类比为加法。类似的,也可以类比得到权值线段树的减法,即对应点数相减。

可持久化线段树

例1:P3834 【模板】可持久化线段树 2

例2:P4587 [FJOI2016]神秘数

考虑对于一个一定确定的可重集合,如何确定其答案。

假设该可重集合的元素从小到大分别为 $a_1, a_2, \cdots, a_{\lvert s \rvert}$,并将元素按顺序加入一个集合 $S$。记录一个变量 $ans$ 代表目前不能被 $S$ 中若干元素求和表示的最小数,当 $S$ 为空时,$ans = 1$。此时,如果向 $S$ 中加入一个元素 $a_i$,如果 $a_i \in [1, ans]$,那么 $ans$ 会变为 $ans + a_i$,否则 $ans$ 不会有变化。

如果 $a_i \gt ans$,则显然数 $ans$ 依然无法被表示;否则,对于 $[1, a_i - 1]$ 的元素按照原样表示,对于 $[a_i, ans + a_i - 1]$(其中 $ans$ 为修改之前的 $ans$)的数,则可以拆分为 $a_i + x$,其中 $x \in [1, ans - 1]$,可以被之前的元素求和表示。

由于每次加入数据时,加入的数必然大于上一次的答案,因此如果可以加入新树,加入完成之后的答案至少大于原答案的两倍。这也就意味着,对于任意一个元素 $a_i$,如果最终可以被加入集合,则之多需要 $\mathcal O(\log a_i)$ 次就会被加入,故单次查询操作的次数为 $\mathcal O(\log(\max \{ a_i \} ))$。

    for (int i = 1; i <= m; i++) {
        std::cin >> l >> r;
        ans = 1;
        lst = 0;
        while (true) {
            res = SGT.Query(SGT.root[l - 1], SGT.root[r], lst + 1, ans);
            lst = ans;
            if (res) ans = ans + res;
            else break;
        }
        std::cout << ans << '\n';
    }

树状数组套权值线段树

例3:P3380 【模板】二逼平衡树(树套树)

如果本题去掉修改操作,则显然可以使用可持久化权值线段树进行维护。又由于可持久化权值线段树可以类比为权值树的前缀和,可以考虑采用类似于维护动态前缀和的方式来维护本题。而对动态前缀和的维护,最简单的方法即为树状数组(或线段树)。

在这里,可以考虑采用嵌套的形式,将权值树作为整体,用树状数组或是线段树对其进行维护。由于权值树同时满足可合并性、可减性和交换律,因此这样的维护是可行的。

以树状数组为例,对序列的每一个位置开一个权值树,然后用树状数组进行维护。每次操作先跑一遍树状数组处理出这次操作涉及到那几棵树,然后直接进行权值树的查询即可。

typedef std::vector<int> svi;

struct BinaryIndexedTree {
    struct SegmentTree {
        struct Node {
            int l, r;
            int lson;
            int rson;
            int val;
        } node[maxN * logN * logN + 10];

        int ncnt;
        int root[maxN + 10];

        void PushUp(int u) {
            node[u].val = node[node[u].lson].val + node[node[u].rson].val;
            return;
        }

        void Build(int &u) {
            u = ++ncnt;
            return;
        }

        void Clone(int &u, int v) {
            u = ++ncnt;
            node[u] = node[v];
            return;
        }

        void Modify(int &u, int l, int r, int pos, int val) {
            if (u == 0) Build(u);
            if (l == r) {
                node[u].val += val;
                return;
            }
            int mid = (l + r) / 2;
            if (pos <= mid) Modify(node[u].lson, l, mid, pos, val);
            else Modify(node[u].rson, mid + 1, r, pos, val);
            PushUp(u);
            return;
        }

        int QueryRank(svi u, svi v, int l, int r, int k) {
            if (l == r) return 0;
            int mid = (l + r) / 2;
            int cnt = 0;
            if (k <= mid) {
                if (u.size()) for (int i = 0; i < u.size(); i++) u[i] = node[u[i]].lson;
                if (v.size()) for (int i = 0; i < v.size(); i++) v[i] = node[v[i]].lson;
                return QueryRank(u, v, l, mid, k);
            } else {
                if (u.size()) for (int i = 0; i < u.size(); i++) cnt -= node[node[u[i]].lson].val;
                if (v.size()) for (int i = 0; i < v.size(); i++) cnt += node[node[v[i]].lson].val;
                if (u.size()) for (int i = 0; i < u.size(); i++) u[i] = node[u[i]].rson;
                if (v.size()) for (int i = 0; i < v.size(); i++) v[i] = node[v[i]].rson;
                return cnt + QueryRank(u, v, mid + 1, r, k);
            }
        }

        int QueryKth(svi u, svi v, int l, int r, int k) {
            if (l == r) return l;
            int mid = (l + r) / 2;
            int cnt = 0;
            for (int i = 0; i < u.size(); i++) cnt -= node[node[u[i]].lson].val;
            for (int i = 0; i < v.size(); i++) cnt += node[node[v[i]].lson].val;
            if (k <= cnt) {
                for (int i = 0; i < u.size(); i++) u[i] = node[u[i]].lson;
                for (int i = 0; i < v.size(); i++) v[i] = node[v[i]].lson;
                return QueryKth(u, v, l, mid, k);
            } else {
                for (int i = 0; i < u.size(); i++) u[i] = node[u[i]].rson;
                for (int i = 0; i < v.size(); i++) v[i] = node[v[i]].rson;
                return QueryKth(u, v, mid + 1, r, k - cnt);
            }
        }
    } SGT;

    int LowBit(int val) {
        return val & -val;
    }

    void Add(int pos, int val) {
        for (int i = pos; i <= n; i += LowBit(i)) {
            SGT.Modify(SGT.root[i], 1, range.size() - 2, a[pos], val);
        }
        return;
    }

    int AskRank(int l, int r, int k) {
        svi u;
        svi v;
        l -= 1;
        while (l) {
            u.push_back(SGT.root[l]);
            l -= LowBit(l);
        }
        while (r) {
            v.push_back(SGT.root[r]);
            r -= LowBit(r);
        }
        return SGT.QueryRank(u, v, 1, tot, k) + 1;
    }

    int AskKth(int l, int r, int k) {
        svi u;
        svi v;
        l -= 1;
        while (l) {
            u.push_back(SGT.root[l]);
            l -= LowBit(l);
        }
        while (r) {
            v.push_back(SGT.root[r]);
            r -= LowBit(r);
        }
        return SGT.QueryKth(u, v, 1, tot, k);
    }

    int AskPre(int l, int r, int k) {
        int rank = AskRank(l, r, k);
        if (rank == 1) return 0;
        return AskKth(l, r, rank - 1);
    }

    int AskSuf(int l, int r, int k) {
        if (k >= range.size() - 1) return tot + 1;
        int rank = AskRank(l, r, k + 1);
        if (rank > r - l + 1) return tot + 1
        return AskKth(l, r, rank);
    }
} BIT;

例4:P3810 【模板】三维偏序(陌上花开)

首先介绍离线算法和在线算法的相关概念。离线算法,即在使用核心算法处理数据前,便已经获得所有的输入数据。本题在开始算法之前已经直接得到了所有元素的所有属性,故可以采用离线算法。离散化就是一种典型的离线算法,因为需要获得所有的数据后才可以将这些数据排序。

与离线算法对应的是在线算法,即在算法开始前不知道所有的输入数据,需要动态处理。在算法竞赛中,有部分题目的输入数据与上一次询问的结果有关,此类题目会要求选手必须使用在线算法完成问题,要求会比离线问题更高。

考虑将问题简化为两个属性(设为 $A, B$)的时候该如何解决。一种简单的方式是,使用离线算法,先按照 $A$ 属性进行排序并按顺序遍历,再用权值线段树对前 $i$ 个元素中的 $B$ 属性进行维护。当插入一个元素时,只要在权值线段树中查找 $B$ 属性小于等于该元素的元素个数,并统计总数即可。当属性个数达到三个时,依然先按照某一元素进行排序,从而将需要考虑的属性减少至两个。

使用树状数组套线段树解决这个问题。在对 $A$ 属性进行排序后,依次遍历所有元素,用外层树状数组维护 $B$ 属性,用内层的线段树维护 $C$ 属性。例如对于一个 $B$ 属性为 $b$,$C$ 属性为 $c$ 的元素,在 $b$ 在树状数组上对应的所有权值树的 $c$ 位置插入一个元素即可。

当遍历到某一元素时,先查询目前已遍历到元素中 $B, C$ 两个属性逗比当前元素属性小的元素个数。若当前元素 $B$ 属性为 $b$,$C$ 属性为 $c$,则等价于查询第 $1$ 到 $b$ 棵树中值域在 $[1, c]$ 值域内到数并统计答案。在完成查询后,将元素插入到树套树中,并按顺序遍历下一元素。

struct Element {
    int a, b, c;
    int id;

    bool operator<(const Element &other) const {
        if (a != other.a) return a < other.a;
        if (b != other.b) return b < other.b;
        return c < other.c;
    }
} e[maxN + 10];
struct BinaryIndexedTree {
    struct SegmentTree {
        struct Node {
            int lson;
            int rson;
            int value;
        } node[maxK * logK * logN + 10];

        int ncnt;
        int root[maxK + 10];

        void PushUp(int u) {
            node[u].value = node[node[u].lson].value + node[node[u].rson].value;
            return;
        }

        void Build(int &u) {
            u = ++ncnt;
            return;
        }

        void Modify(int &u, int l, int r, int weight, int value) {
            if (!u) Build(u);
            if (l == r) {
                node[u].value += value;
                return;
            }
            int mid = (l + r) / 2;
            if (weight <= mid) Modify(node[u].lson, l, mid, weight, value);
            else Modify(node[u].rson, mid + 1, r, weight, value);
            PushUp(u);
            return;
        }

        int Query(int u, int l, int r, int weight) {
            if (l == r) return node[u].value;
            int mid = (l + r) / 2;
            if (weight <= mid) return Query(node[u].lson, l, mid, weight);
            else return node[node[u].lson].value + Query(node[u].rson, mid + 1, r, weight);
        }
    } SGT;

    int lowbit(int x) {
        return x & -x;
    }

    void Add(int pos, int weight, int value) {
        for (int i = pos; i <= k; i += lowbit(i)) {
            SGT.Modify(SGT.root[i], 1, k, weight, value);
        }
        return;
    }

    int Ask(int pos, int weight) {
        int ret = 0;
        for (int i = pos; i > 0; i -= lowbit(i)) {
            ret += SGT.Query(SGT.root[i], 1, k, weight);
        }
        return ret;
    }
} BIT;
    std::sort(e + 1, e + n + 1);
    int i = 1;
    int j;
    while (i <= n) {
        j = i;
        while (e[i].a == e[j].a) BIT.Add(e[j].b, e[j].c, 1), j++;
        j = i;
        while (e[i].a == e[j].a) f[e[j].id] = BIT.Ask(e[j].b, e[j].c), j++;
        i = j;
    }
    for (int i = 1; i <= n; i++) d[f[i]]++;
    for (int i = 1; i <= n; i++) std::cout << d[i] << '\n';