zoukankan      html  css  js  c++  java
  • [数据结构]可持久化线段树

    #1.0 何为可持久化线段树


    也有人叫它 “主席树”,参见 知乎讨论

    至于为什么,别问,问就是枪毙加急许可。

    (以上纯属笔者乱搞,下面正文开始)


    都知道线段树可以进行单点、区间修改等操作,但是如果我们已经做了 (114514) 次操作,突然想要查询在第 (2333) 次修改后的信息呢?那我们就需要保存每一次修改的信息了,这也就是 “可持久化” 的意义。

    嘛,总之就是这样,似乎没有多么麻烦的样子,下面来看怎么实现。

    #2.0 如何实现

    为了下面更方便叙述,这里用一个 简单的题目 作为例题。

    如题,你需要维护这样的一个长度为 (N) 的数组,支持如下几种操作

    1. 在某个历史版本上修改某一个位置上的值
    2. 访问某个历史版本上的某一位置的值

    此外,每进行一次操作(对于操作 (2),即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从 (1) 开始编号,版本 (0) 表示初始状态数组)

    上面这个问题抛去“历史版本”不看,可以线段树单点修改维护,不多说,还是来看如何可持久化。

    #2.1 朴素想法

    最最最最最朴素的想法就是直接将整个修改过的线段树,复制一遍,存下来(话说笔者最初真没想到这个,直接跳过这一步了...),但是很显然这么做空间、时间都不允许。

    #2.2 最终做法

    没想到吧,就是这么快...小编也很惊讶,但事实就是这样。

    我们注意到上面的操作存下来了许多没有用的信息,明明有很多点没有被修改,却也被复制了一遍,就好比明明你的学长在机房颓废,但你没有,教练却把所有人都训了一顿一样无理。

    那怎么办?很简单,只存修改了的节点不就好了!

    显然根节点一定会改变,那么递归时,如果左右儿子之一没改变,就直接连上就好了,只去递归被修改的子节点,对这个子节点复制一份,在这个副本上进行修改,并连接到这个副本。

    当然,对于上题中的操作 (2),直接将该操作对应的根节点设置为要查询的那个历史版本就好了。

    以上就是所有的额外操作了,非常简单对吧!

    为了保证空间,所以建议采用 动态开点 的形式。

    #2.3 完整代码

    好久没写过单点查询单点修改的线段树了...突然有点忘了咋写...

    const int N = 4000100;
    const int INF = 0x3fffffff;
    
    struct Node {
        int l, r;
        int ls, rs;
        int sum;
    };
    Node p[N << 2];
    
    int n, a[N], cnt, rt[N], m;
    
    void build(int k, int l, int r) {
        p[k].l = l, p[k].r = r;
        if (l == r) {p[k].sum = a[l]; return;}
        int mid = (l + r) >> 1;
        p[k].ls = ++ cnt;
        build(p[k].ls, l, mid);
        p[k].rs = ++ cnt;
        build(p[k].rs, mid + 1, r);
    }
    
    void modify(int t, int &k, int x, int c) {
        if (!k) k = ++ cnt;
        p[k].l = p[t].l, p[k].r = p[t].r;
        if (p[t].l == p[t].r) {p[k].sum = c; return;}
        int mid = (p[k].l + p[k].r) >> 1;
        if (x <= mid) {
            p[k].rs = p[t].rs;
            modify(p[t].ls, p[k].ls, x, c);
        } else {
            p[k].ls = p[t].ls;
            modify(p[t].rs, p[k].rs, x, c);
        }
    }
    
    int query(int k, int x) {
        if (p[k].l == p[k].r) return p[k].sum;
        int mid = (p[k].l + p[k].r) >> 1;
        if (x <= mid) return query(p[k].ls, x);
        else return query(p[k].rs, x);
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i ++)
          scanf("%d", &a[i]);
        build(rt[0] = ++ cnt, 1, n);
        int v, op, pos, val;
        for (int i = 1; i <= m; i ++) {
            scanf("%d%d%d", &v, &op, &pos);
            if (op == 1) {
                scanf("%d", &val);
                modify(rt[v], rt[i], pos, val);
            } else {
                rt[i] = rt[v];
                printf("%d
    ", query(rt[v], pos));
            }
        }
        return 0;
    }
    

    #3.0 主席树

    在文章的开头,我们就提到了“主席树”这个称呼,有一点是比较确定的,“主席树”指的是可持久化权值线段树

    #3.1 经典问题

    来看一个主席树的经典问题吧:静态区间第 (k)

    给定 (n) 个整数构成的序列 (a),将对于指定的闭区间 ([l,r]) 查询其区间内的第 (k) 小值。

    这是一个主席树的经典应用,我们考虑将序列中的数一个一个依次插入主席树,那么,我们不难发现,在插入第 (r) 个数后的主席树减去插入第 (l-1) 个数后的主席树形成的线段树便是只包含了区间 ([l,r]) 中的数,那么可以用权值线段树查询区间第 (k) 小这一基本操作来解决这题。

    #3.2 代码实现

    首先是单点修改,维护区间和,这个操作与可持久化线段树的操作的基本构架相同。

    void modify(int lt, int &k, int l, int r, int x) {
        if (!k) k = ++ cnt;
        p[k].sum = p[lt].sum + 1;
        /*因为是维护区间数的个数
        所以可以直接更改,不必 pushup*/
        if (l == r) return;
        int mid = (l + r) >> 1;
        if (x <= mid) {
            p[k].rs = p[lt].rs;
            modify(p[lt].ls, p[k].ls, l, mid, x);
        } else {
            p[k].ls = p[lt].ls;
            modify(p[lt].rs, p[k].rs, mid + 1, r, x);
        }
    }
    

    然后是查询操作。这里上文提到的“减去”操作就是减法操作,我们可以得到分离出的线段树的左子树上数的个数 sum_l ,当前寻找第 k 小,如果 sum_l>=k,那么就意味着,第 k 小在左子树,应当递归左子树,k 不变;否则第 k 小在右子树,应当递归右子树,并令 k=k-sum_l;重复以上操作,直到递归到叶节点。

    int query(int lt, int k, int l, int r, int x) {
        if (l == r) return l;
        int mid = (l + r) >> 1;
        int sum_l = p[p[k].ls].sum - p[p[lt].ls].sum;
        if (sum_l >= x) return query(p[lt].ls, p[k].ls, l, mid, x);
        else return query(p[lt].rs, p[k].rs, mid + 1, r, x - sum_l);
    }
    

    最主要的两个操作也就是这些,因为 (a) 中的数的大小可能很大,需要进行离散化处理,这里附上完整程序。

    对于每一个询问,我们有以下两种处理方式:

    1. 将询问离线,按右端点排序,在插入每个数后检查是否有右端点为此数的,如果有,进行查询操作。
    2. 将所有数一并插入,依次处理每个询问。

    这里采用第一种处理方法。

    const int N = 1000100;
    const int INF = 0x3fffffff;
    
    struct Node {
        int ls, rs;
        int sum;
    };
    Node p[N << 2];
    
    struct Ques {
        int l, r, k, idx;
        bool operator < (const Ques &b) const {
            return r < b.r;
        }
    };
    Ques q[N];
    
    int n, m, a[N], cnt, t[N], tot, rt[N], ans[N];
    
    inline void pushup(int k) {
        p[k].sum = p[p[k].ls].sum + p[p[k].rs].sum;
    }
    
    void build(int &k, int l, int r) {
        if (!k) k = ++ cnt;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(p[k].ls, l, mid);
        build(p[k].rs, mid + 1, r);
    }
    
    void modify(int lt, int &k, int l, int r, int x) {
        if (!k) k = ++ cnt;
        p[k].sum = p[lt].sum + 1;
        if (l == r) return;
        int mid = (l + r) >> 1;
        if (x <= mid) {
            p[k].rs = p[lt].rs;
            modify(p[lt].ls, p[k].ls, l, mid, x);
        } else {
            p[k].ls = p[lt].ls;
            modify(p[lt].rs, p[k].rs, mid + 1, r, x);
        }
    }
    
    int query(int lt, int k, int l, int r, int x) {
        if (l == r) return l;
        int mid = (l + r) >> 1;
        int sum_l = p[p[k].ls].sum - p[p[lt].ls].sum;
        if (sum_l >= x) return query(p[lt].ls, p[k].ls, l, mid, x);
        else return query(p[lt].rs, p[k].rs, mid + 1, r, x - sum_l);
    }
    
    inline int mpg(int x) { //离散化后进行映射
        return lower_bound(t + 1, t + tot + 1, x) - t;
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i ++) {
            scanf("%d", &a[i]);
            t[i] = a[i];
        }
        for (int i = 1; i <= m; i++) {
            scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k);
            q[i].idx = i;
        }
        /*对询问排序及对 a 进行离散化*/
        sort(q + 1, q + m + 1);
        sort(t + 1, t + n + 1);
        tot = unique(t + 1, t + n + 1) - t - 1;
        build(rt[0], 1, tot); //建空树
        for (int i = 1, j = 1; i <= n; i ++) {
            modify(rt[i - 1], rt[i], 1, tot, mpg(a[i]));
            while (q[j].r == i)
              ans[q[j].idx] = query(rt[q[j].l - 1], rt[i], 1, tot, q[j].k), j ++;
        }
        for (int i = 1; i <= m; i ++)
          printf("%d
    ", t[ans[i]]);
        return 0;
    }
    
    

    参考资料

    [1] 良心的可持久化线段树教程 - 胡小兔

    [2] 可持久化线段树 - OI Wiki

  • 相关阅读:
    python 列表、元组、字典总结
    python 字典
    python 元组
    python 列表
    JMeter的作用域与执行顺序
    JMeter第一个实战
    JMeter录制的两种方法
    JMeter常用功能
    初识jmeter2
    handler的拒绝策略:
  • 原文地址:https://www.cnblogs.com/Dfkuaid-210/p/14970925.html
Copyright © 2011-2022 走看看