首页 > 其他分享 >数据结构——线段树优化 学习笔记

数据结构——线段树优化 学习笔记

时间:2024-08-07 15:17:11浏览次数:7  
标签:ac ms int 线段 笔记 loj https 数据结构

数据结构——线段树优化 学习笔记

比较基础,因此讲的很快。

我们主要关注单点修改、区间查询的线段树,这是应用最广泛的。

线段树问题

我们以 LOJ 的这道题为例,

例题:LOJ #130. 树状数组 1 :单点修改,区间查询

洛谷上面也有类似的题:P3374 【模板】树状数组 1

因为洛谷的题的数据范围较小,我们使用更强的 LOJ 的题。

普通线段树

线段树首先可以分为,

  • 指针式线段树,就是使用指针「动态开点」进行操作。

  • 数组式线段树,就是使用多个数组记录信息。

  • 结构体式线段树,就是使用一个结构体来记录多个信息。

一般来说速度依次递增,我们此处考虑这种线段树怎么优化。

Version #1:朴素版

我们不考虑指针式线段树,先看一个普通的数组式线段树的代码:

ll sum[N << 2];

void push_up(int k) {
    sum[k] = sum[k << 1] + sum[k << 1 | 1];
}

void build(int k, int l, int r) {
    sum[k] = 0;
    if (l == r) {
        sum[k] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(k << 1, l, mid);
    build(k << 1 | 1, mid + 1, r);
    push_up(k);
}

void modify(int k, int l, int r, int x, int v) {
    if (l == r) {
        sum[k] += v;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) modify(k << 1, l, mid, x, v);
    else modify(k << 1 | 1, mid + 1, r, x, v);
    push_up(k);
}

ll query(int k, int l, int r, int p, int q) {
    if (l >= p && r <= q) return sum[k];
    int mid = (l + r) >> 1;
    ll res = 0;
    if (p <= mid) res += query(k << 1, l, mid, p, q);
    if (q >= mid + 1) res += query(k << 1 | 1, mid + 1, r, p, q);
    return res;
}

它在 LOJ 上面跑了 571 ms(https://loj.ac/s/2128355)。

Version #2:非递归化

注意到单点修改只是在树上走出了一条从上到下的路径,因此可以非递归处理:

void modify(int x, int v) {
    int k = 1;
    int l = 1, r = n;
    while (l < r) {
        sum[k] += v;
        int mid = (l + r) >> 1;
        if (x <= mid) r = mid, k = k << 1;
        else l = mid + 1, k = k << 1 | 1;
    }
    sum[k] += v;
}

这样就优化到了 493 ms(https://loj.ac/s/2128360)。

zkw 线段树

更详细的见我另一个博客。

我们堆式建树,并钦定值域为 \([0,K]\),其中 \(K\) 为 \(>N\) 的最小的 \(2\) 的整数次幂。

Version #3:自底向上

根据二进制 + 线段树的结构性质,我们可以写出代码:

int m;

void build() {
    m = 1 << (__lg(n) + 1);
    for (int i = 1; i <= n; ++i) sum[m + i] = a[i];
    for (int i = m - 1; i; --i) push_up(i);
}

void modify(int x, int v) {
    x += m;
    while (x) {
        sum[x] += v;
        x >>= 1;
    }
}

ll query(int p, int q) {
    p += m - 1, q += m + 1;
    ll s = 0;
    while (p ^ q ^ 1) {
        if (p % 2 == 0) s += sum[p ^ 1];
        if (q % 2 == 1) s += sum[q ^ 1];
        p >>= 1, q >>= 1;
    }
    return s;
}

跑到了 328 ms(https://loj.ac/s/2128362)。

Version #4:分支消除

我们知道在 C++ 中,对于分支的优化是较小的。

因此,我们使用三元运算符或者乘法来替代 if 的分支。

ll query(int p, int q) {
    p += m - 1, q += m + 1;
    ll s = 0;
    while (p ^ q ^ 1) {
        s += (p % 2 == 0) * sum[p ^ 1];
        s += (q % 2 == 1) * sum[q ^ 1];
        p >>= 1, q >>= 1;
    }
    return s;
}

这样就是 286 ms(https://loj.ac/s/2128365)。

树状数组

树状数组可以理解为去掉柚子厨的 zkw 线段树。

这个操作可以使其空间减半,同时带上 \(1/2\) 的巨小常数。

Version #5:位运算优化

注意到减去 \(\operatorname{lowbit}\) 的过程,等价于位与本身减一。

void modify(int x, int v) {
    for (; x <= n; x += x & -x)
        sum[x] += v;
}

ll query(int x) {
    ll r = 0;
    for (; x; x &= x - 1)
        r += sum[x];
    return r;
}

ll query(int p, int q) {
    return query(q) - query(p - 1);
}

跑了 258 ms(https://loj.ac/s/2128367)。

Version #6:线性建树

方法一:尝试倒过来,把叶子结点的值直接传给父亲。

void build() {
    for (int i = 1; i <= n; ++i) {
        sum[i] += a[i];
        int j = i + (i & -i);
        if (j <= n)
            sum[j] += sum[i];
    }
}

这样是 241 ms(https://loj.ac/s/2128373),很快的。

方法二:根据树状数组每个节点记录的区间,前缀和处理。

ll pre[N], sum[N];

void build() {
    for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + a[i];
    for (int i = 1; i <= n; ++i) sum[i] = pre[i] - pre[i - (i & -i)];
}

跑了 245 ms(https://loj.ac/s/2128375)。

Version #7:缓存优化

理论见 HPC Fenwick Trees,因为我自己没研究懂。

在所有关于 sum 的操作上都加上 hole(x)

inline constexpr int hole(int k) {
    return k + (k >> 10);
}

void build() {
    for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + a[i];
    for (int i = 1; i <= n; ++i) sum[hole(i)] = pre[i] - pre[i - (i & -i)];
}

void modify(int x, int v) {
    for (; x <= n; x += x & -x)
        sum[hole(x)] += v;
}

ll query(int x) {
    ll r = 0;
    for (; x; x &= x - 1)
        r += sum[hole(x)];
    return r;
}

这样能快不少,234 ms(https://loj.ac/s/2128379)。

WTree 不会。

Reference

https://en.algorithmica.org/hpc/data-structures/segment-trees/

标签:ac,ms,int,线段,笔记,loj,https,数据结构
From: https://www.cnblogs.com/RainPPR/p/18347100

相关文章

  • 数据结构——线段树提高 学习笔记
    数据结构——线段树提高学习笔记一些比较系统的东西,会单独放文章,这里只写一些理论的。线段树维护矩阵例题:P7453[THUSCH2017]大魔法师。当区间信息比较复杂,但是满足结合律的时候,可以使用矩阵维护。线段树每个节点维护一个矩阵,合并区间时使用矩阵乘法转移。但是,矩阵乘法的......
  • k8s学习笔记之CoreDNS
    一、CoreDNSconfigMap配置参数及说明主要讲解CoreDNSconfigMap及其他关键配置部署文件(需要替换其中一些变量):https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/dns/coredns/coredns.yaml.base1.configMap配置apiVersion:v1kind:ConfigMapmetadat......
  • java笔记6
    10.多态多态的概念多态(Polymorphism)是面向对象编程的核心概念之一,它指的是同一个接口可以被多个不同的类实现,或者同一个操作作用于不同的对象时可以有不同的解释和行为。为何要用多态多态的使用使得代码更加灵活和可扩展,它允许编写的代码可以对不同类型的对象执行不同的操作。......
  • Living-Dream 系列笔记 第76期
    UVA1328简单题。我们有结论:对于一个周期串S的子串T,它的最小循环节即为T-nxt_{\left|T\right|}。(具体请查阅往期笔记)于是,我们枚举所有前缀,检验上式是否能被当前前缀的长度整除并且不止一个循环节即可。code#include<bits/stdc++.h>usingnamespacestd;constintN=......
  • 数据结构 - 并查集路径压缩
    ......
  • 【数据结构与算法】删除循环队列中第k个元素的算法 C++实现(循环队列+模运算)
    数组a[MaxSize]用作一个循环队列,front指向循环队列中队头元素的前一个位置,rear指向队尾元素的位置。设计删除队列中第k个元素的算法。思路首先,判断kkk是否在有效范围内......
  • 【数据结构与算法】在循环队列中第k个元素之后插入元素的算法 C++实现(循环队列+模运算
    数组a[MaxSize]用作一个循环队列,front指向循环队列中队头元素的前一个位置,rear指向队尾元素的位置。设计在队列中第k个元素之后插入item的算法。思路首先,检查输入的位置k是否在合理的范围内,即1到queueSize(Q)(包含两端)。如果k在这个范围外,那么返回ERROR。然后,计......
  • 笔记:从Aurora 8b/10b 到Aurora 64b/66b (三):自定义PHY层收发
    相较于8/10来说没那么复杂,需要考虑的情况只有八种;但是gearbox的控制需要额外的心思:每三十二周期所有操作都需要停止;这一点在收发都需要注意;RX:核心思想是利用header做检测,将夹杂在数据流中的控制包滤除掉;modulegt_phy_rx(inputwirei_rx_clk......
  • 《计算机网络 - 自顶向下方法》阅读笔记
    《计算机网络-自顶向下方法》阅读笔记应用层、运输层、网络层、数据链路层计算机网络和因特网:因特网:​ 是一个世界范围的计算机网络,互联了全世界的计算机设备计算机设备:手机,电脑,游戏机,电视等所有这些设备都称为主机(host)或端系统(endsystem)端系统通过通信......
  • 【题解】Solution Set - NOIP2024集训Day1 数据结构
    【题解】SolutionSet-NOIP2024集训Day1数据结构https://www.becoder.com.cn/contest/5429「CF1428F」FruitSequences线段树是可以维护区间最长子段的1。记固定右端点在\(i\),的答案为\(f_i\)。那么:\(a_i=0\),\(f_i=f_{i-1}\);\(a_i=1\),打一个单调栈维护所有的最长子......