线段树

线段树

1. 线段树原理

原理

  • 线段树存在五个操作

    (1)pushup:由子节点计算父节点的信息;

    (2)pushdown:把当前父节点的修改信息下传到子节点,也被称为懒标记(延迟标记);这个操作比较复杂,一般不涉及到区间修改则不用写。

    (3)build:将一段区间初始化成线段树;

    (4)modify:修改操作,分为两类:① 单点修改(需要使用pushup),② 区间修改(需要使用pushdown);

    (5)query:查询一段区间的值。

  • 线段树的原理:线段树除了最后一层之外,是一颗满二叉树,假设区间中存在n个数据,则倒数第二层节点数大于为n,从第一层到倒数第三层的节点数大约为n-1,最后一层节点数很少,但是为了使用数组存储整棵树,最后一层大约需要开2n的空间,因此一共需要开辟4n的空间存储线段树。

在这里插入图片描述

  • 根节点存储在数组下标为1的位置;因为线段树存储的数据下标是从1开始的,所以原数组下标最好从1开始,不是的话需要加上一个偏移量。
  • 对于第(3)个操作的伪代码:
// 创建以节点u为根的线段树,根节点对应区间为[l, r]
void build(int u, int l, int r) {
    tr = {l, r};
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    // pushup(u);
}
  • 对于第(5)个操作,比如我们要查询最大值,查询[5, 9]之间数据的最大值,则对于不同的情况,我们需要分类讨论,假设我们当前插叙区间[l, r]的最大值,此时[l, r]存在一个 [ T l , T r ] [T_l, T_r] [Tl,Tr]

    (1)若 [ T l , T r ] ⊂ [ l , r ] [T_l, T_r] \subset [l, r] [Tl,Tr][l,r],则直接取 [ T l , T r ] [T_l, T_r] [Tl,Tr]的最大值作为候选最大值;

    (2)若 [ T l , T r ] ⊄ [ l , r ] [T_l, T_r] \not \subset [l, r] [Tl,Tr][l,r] [ T l , T r ] ⋂ [ l , r ] ≠ ∅ [T_l, T_r] \bigcap [l, r] \neq \emptyset [Tl,Tr][l,r]=,和左边有交集的话递归到左边求解,和右边有交集的话递归到右边求解,都有交集的话都求解。

    (3)不存在 [ T l , T r ] ⋂ [ l , r ] = ∅ [T_l, T_r] \bigcap [l, r] = \emptyset [Tl,Tr][l,r]=的情况,因为我们只有在有交集的情况下才会递归求解,没有交集就不会递归求解了。

  • 查询[5, 9]之间数据的最大值会遍历到的节点如下图橙色圈标出的:

在这里插入图片描述

  • 这里的操作时间复杂度大约在 4 × l o g ( n ) 4 \times log(n) 4×log(n)的量级上,常数大于树状数组,下面以查询操作为例,分析一下时间复杂度:

    (1)若 [ T l , T r ] ⊂ [ l , r ] [T_l, T_r] \subset [l, r] [Tl,Tr][l,r],则直接返回 [ T l , T r ] [T_l, T_r] [Tl,Tr]的最大值,不会再递归求解了;

    (2)若 [ T l , T r ] ⊄ [ l , r ] [T_l, T_r] \not \subset [l, r] [Tl,Tr][l,r] [ T l , T r ] ⋂ [ l , r ] ≠ ∅ [T_l, T_r] \bigcap [l, r] \neq \emptyset [Tl,Tr][l,r]=,和左边有交集的话递归到左边求解,和右边有交集的话递归到右边求解,都有交集的话都求解。

    在这里插入图片描述

  • 对于第(4)个操作,如果只考虑单点修改的话,从叶子节点向上递归修改(单点对应pushup)即可。

2. AcWing上的线段树题目

AcWing 1275. 最大数

问题描述

分析

  • 因为最多存在 2 × 1 0 5 2 \times 10^5 2×105个操作,我们开始可以将所有的坑位全部开好,而不是一个一个添加,然后使用一个变量n记录当前有多少位置被占用了。

  • 则上面的添加操作可以看成在第n+1个位置的添加一个数。相当于将当前位置的数修改为指定数据。因此,题目中的两个操作可以变为:

    (1)修改某个位置的数据;

    (2)查询区间 [ n − L + 1 , n ] [n-L+1, n] [nL+1,n]数的最大值;

  • 典型的线段树的操作,可以使用线段树解决。

  • 线段树的每个节点一般使用结构体实现,我们必须有存储区间的左右端点l和r,另外还需要存储节点的某种属性(可能是多种),问的属性一般都要存储下来,比如本题问最大值,因此区间[l, r]之间的最大值要存下来;另外还要存储哪些属性呢?

  • 其实遵守一个原则即可:根据我们所存储的属性,能求出父节点的这些属性

  • 我们发现,对于本题而言,存储子节点的最大值足以求出父节点的最大值了,因此存储最大值就足已。

代码

  • C++
#include <cstdio>
#include <iostream>

using namespace std;

const int N = 200010;

int m, p;  // 操作数, 取模的数
struct Node {
    int l, r;
    int v;  // 区间[l, r]中的最大值
} tr[N * 4];

void pushup(int u) {  // 由子节点的信息,来计算父节点的信息
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}

// 节点tr[u]存储区间[l, r]的信息
void build(int u, int l, int r) {
    
    tr[u] = {l, r};
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    // pushup(u);  // 不需要这句话,因为线段树中所有的v都是0
}

// 从节点tr[u]开始查询区间[l, r]的最大值
int query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;  // 树中节点,已经被完全包含在[l, r]中了
    
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if (l <= mid) v = query(u << 1, l, r);
    if (r > mid) v = max(v, query(u << 1 | 1, l, r));  // 右区间从mid+1开始
    
    return v;
}

// 从节点tr[u]开始修改第x个数为v
void modify(int u, int x, int v) {
    
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);  // 修改子节点,则必须更新父节点
    }
}

int main() {
    
    int n, last;  // n:插入的数据的个数,last:上次询问的结果
    scanf("%d%d", &m, &p);
    
    // 创建线段树, 根节点是tr[1]
    build(1, 1, m);
    
    char op[2];  // 操作码
    int x;  // 操作数
    while (m--) {
        scanf("%s%d", op, &x);
        if (*op == 'Q') {
            // 从根节点1开始查询区间[n-x+1, n]的最大值
            last = query(1, n - x + 1, n);
            printf("%d\n", last);
        } else {
            // 从根节点1开始修改第n+1个数为(x + last) % p
            modify(1, n + 1, (x + last) % p);
            n++;
        }
    }
    
    return 0;
}

AcWing 245. 你能回答这些问题吗

问题描述

分析

  • 单点修改,modify中使用pushup即可。
  • 查询区间内的最大连续子段和。我们要考虑每个节点内部需要存储哪些信息?首先,必须存储区间端点和对应的最大连续子段和,如下:
struct Node {
    int l, r;  // 区间左右端点
    int tmax;  // 最大连续子段和
}
  • 现在考虑我们能不能使用上述信息从子节点得到父节点的最大连续子段和呢?答案是不能的,因为我们不能求出跨两个子区间的最大连续子段和,因此还需要添加属性,需要添加该区间的最大后缀和以及最大前缀和,如下:
struct Node {
    int l, r;  // 区间左右端点
    int tmax;  // 最大连续子段和
    int lmax;  // 最大前缀和
    int rmax;  // 最大后缀和
}

在这里插入图片描述

则此时父节点的最大连续子段和是左右孩子的tmax以及上述情况的最大值。

  • 我们现在新加入了两个属性,此时还需要考虑能否从子节点得到父节点的这两个属性呢?答案是不能的,因为如果父节点的lmax或者rmax横跨两个子节点,则无法从子节点得到父节点的这两个属性,因此还需要增加属性,增加整个区间的总和,如下:
struct Node {
    int l, r;  // 区间左右端点
    int tmax;  // 最大连续子段和
    int lmax;  // 最大前缀和
    int rmax;  // 最大后缀和
    int sum;  // 区间总和
}

在这里插入图片描述

此时根据这些数据,可以从子节点计算出父节点的tmax, lmax, rmax,那新加入的sum能否计算出来呢?这是可以的,因为父节点的sum等于两个子节点的sum和。

  • 至此,本题分析完毕。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 500010;

int n, m;  // 数列长度、指令条数
int a[N];  // 输入数组
struct Node {
    int l, r;
    // 区间总和、最大前缀和、最大后缀和、最大连续子段和
    int sum, lmax, rmax, tmax;
} tr[N * 4];

// 根据u的左右孩子构造u
void pushup(Node &u, Node &l, Node &r) {
    
    u.sum = l.sum + r.sum;
    u.lmax = max(l.lmax, l.sum + r.lmax);
    u.rmax = max(r.rmax, l.rmax + r.sum);
    u.tmax = max(max(l.tmax, r.tmax), l.rmax + r.lmax);
}

void pushup(int u) {
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

void build(int u, int l, int r) {
    
    if (l == r) tr[u] = {l, r, a[l], a[l], a[l], a[l]};
    else {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

// 从节点tr[u]开始修改第a[x]个数为v
void modify(int u, int x, int v) {
    
    if (tr[u].l == tr[u].r) tr[u] = {x, x, v, v, v, v};
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

// 返回从节点tr[u]开始查询区间a[l, r]的最大连续子段和所在的节点
Node query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (r <= mid) return query(u << 1, l, r);
        else if (l > mid) return query(u << 1 | 1, l, r);
        else {
            auto left = query(u << 1, l, r);
            auto right = query(u << 1 | 1, l, r);
            Node res;
            pushup(res, left, right);  // 根据左右孩子求解tmax
            return res;
        }
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
    build(1, 1, n);
    
    int k, x, y;
    while (m--) {
        scanf("%d%d%d", &k, &x, &y);
        if (k == 1) {
            if (x > y) swap(x, y);
            printf("%d\n", query(1, x, y).tmax);
        } else {
            modify(1, x, y);
        }
    }
    
    return 0;
}

AcWing 246. 区间最大公约数

问题描述

分析

  • 本题中存在两个操作

    (1)区间[L, R]增加一个数;

    (2)求区间内的最大公约数;

  • 这是一个区间修改,区间查询的问题,我们要考虑每个节点内部需要存储哪些信息?首先,必须存储区间端点和对应的最大公约数,如下:

struct Node {
    int l, r;
    int d;  // 区间[l, r]之间数据的最大公约数
}
  • 现在考虑我们能不能使用上述信息从子节点得到父节点的最大公约数呢?答案是可以的,父节点的最大公约数等于所有子节点最大公约数的最大公约数。因此,如果只有查询的话,维护一个最大公约数就足已;但是,我们还有修改操作,我们需要考虑怎么处理修改的情况。
  • 这里可以使用差分的思想,假设我们使用小括号表示数据的最大公约数,即 ( a , b ) (a, b) (a,b)表示a和b的最大公约数,下面证明:

( a 1 , a 2 , a 3 . . . , a n ) = = ( a 1 , a 2 − a 1 , a 3 − a 2 , . . . , a n − a n − 1 ) (a_1, a_2, a_3 ..., a_n) == (a1, a_2-a_1, a_3-a_2, ..., a_n-a_{n-1}) (a1,a2,a3...,an)==(a1,a2a1,a3a2,...,anan1)

(1)首先证明左边 ≤ \le 右边,假设d是左边的公约数,则d一定是右边的最大公约数;

(2)再证明左边 ≥ \ge 右边,假设d是右边的最大公约数,则d能整除 a 1 a_1 a1,因为d能整除 a 2 − a 1 a_2-a_1 a2a1,所以d能整除 ( a 2 − a 1 ) + a 1 = a 2 (a_2-a_1)+a_1=a_2 (a2a1)+a1=a2,以此类推d能整除 a 3 , . . . , a n a_3,...,a_n a3,...,an

  • 因此我们可以使用线段树维护原数组a对应的差分数组b( b [ i ] = a [ i ] − a [ i − 1 ] b[i] = a[i]-a[i-1] b[i]=a[i]a[i1]),求解差分数组的最大公约数等价于求解原数组的最大公约数。
  • 当我们要求解a[L, R]的最大公约数时,我们返回gcd(a[L], gcd(b[L+1]~b[R]))即可。
  • 当我们给区间[L, R]增加一个数时,我们只需要修改b[L]和b[R+1]的值即可,这样我们的操作就变成了单点修改,区间查询的问题了,不需要用到pushdown操作了。
  • 对于gcd(a[L], gcd(b[L+1]~b[R]))中的a[L]的求解,相当于区间修改,单点查询的问题,这对应AcWing 242. 一个简单的整数问题,可以参考本题的分析:网址。可以使用树状数组或者线段树求解,两者都需要将a转为差分数组。这样对a[L]的求解就可以转换为单点修改,区间查询的问题了。
  • 为了维护a[L]的值,每个节点还需要存储另外一个变量sum,如下:
struct Node {
    int l, r;
    int d;  // 区间a[l, r]之间数据的最大公约数,等价于b[l, r]之间数据的最大公约数
    int sum;  // sum = b[r] + b[r-1] + ... + b[l]
}
  • 现在考虑我们能不能使用上述信息从子节点得到父节点的sum值呢?答案是可以的,父节点的sum等于两个子节点的sum之和。
  • 至此,本题分析完毕。

代码

  • C++
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 500010;

int n, m;  // 数组长度、指令数目
LL a[N];  // a是输入数组, b是a对应的差分数组
struct Node {
    int l, r;
    LL sum;  // sum = b[r] + b[r-1] + ... + b[l]
    LL d;  // 区间a[l, r]之间数据的最大公约数,等价于b[l, r]之间数据的最大公约数
} tr[N * 4];

LL gcd(LL a, LL b) {
    return b ? gcd(b, a % b) : a;
}

void pushup(Node &u, Node &l, Node &r) {
    
    u.sum = l.sum + r.sum;
    u.d = gcd(l.d, r.d);
}

void pushup(int u) {
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

void build(int u, int l, int r) {
    
    if (l == r) {
        LL b = a[r] - a[r - 1];
        tr[u] = {l, r, b, b};
    } else {
        tr[u].l = l, tr[u].r = r;
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

void modify(int u, int x, LL v) {
    
    if (tr[u].l == x && tr[u].r == x) {
        LL b = tr[u].sum + v;
        tr[u] = {x, x, b, b};
    } else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

Node query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (r <= mid) return query(u << 1, l, r);
        else if (l > mid) return query(u << 1 | 1, l, r);
        else {
            auto left = query(u << 1, l, r);
            auto right = query(u << 1 | 1, l, r);
            Node res;
            pushup(res, left, right);
            return res;
        }
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
    
    build(1, 1, n);
    
    int l, r;
    LL d;  // 加上的数
    char op[2];  // 操作
    while (m--) {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'Q') {
            auto left = query(1, 1, l);  // 为了得到a[l]的值,即a[l]=left.sum
            Node right({0, 0, 0, 0});
            if (l + 1 <= r) right = query(1, l + 1, r);  // 为了得到a[l+1~r]的最大公约数
            printf("%lld\n", abs(gcd(left.sum, right.d)));
        } else {
            scanf("%lld", &d);
            modify(1, l, d);
            if (r + 1 <= n) modify(1, r + 1, -d);
        }
    }
    
    return 0;
}

AcWing 243. 一个简单的整数问题2

问题描述

分析

  • 本题对应的是区间加,区间查询问题,可以转化为单点加,区间查询的问题,具体可以参考:树状数组。这里使用线段树解决这个问题。
  • 本题需要用到线段树五个操作中最复杂的一个,即pushdown:把当前父节点的修改信息下传到子节点,也被称为懒标记(延迟标记)。
  • 对于区间修改,最坏的情况下,时间复杂度是 O ( n ) O(n) O(n)的,比如将整个区间修改,这是我们不能接受的,因此pushdown操作应运而生。其核心思想是懒标记,即当树中某个区间已经完全被我们修改的区间包含了,就不再递归下去,直接返回,同时在该节点标记上需要加上一个数。对于本题来说,下面是懒标记的具体用法。
struct Node {
    int l, r;  // 区间左右端点
    int sum;  // 如果考虑当前节点及子节点上的所有标记,其区间[l, r]的总和就是sum
    int add;  // 懒标记,表示需要给以当前节点为根的子树中的每一个节点都加上add这个数(不包含当前节点)
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TR38qQ5-1617890497424)(线段树.assets/image-20210408143505673.png)]

  • 通过这样的操作,修改的时间复杂度也变成了 O ( l o g ( n ) ) O(log(n)) O(log(n))了。
  • 这样做之后,我们的查询操作(query)也要跟着变化,如下图:

在这里插入图片描述

这个操作对应到代码上是(当前节点是root,左孩子是left,右孩子是right):

void pushdown(int u) {
    auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
    if (root.add) {
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
        root.add = 0;
    }
}
  • 修改(modify)操作,如果当前考察的整个区间都要加上一个数,则可以直接加上,就不需要进行pushdown操作了;否则也要进行类似于上面的pushdown操作。
void modify(int u, int l, int r, int d) {
    if (tr[u].l >= l && tr[u].r <= r) {  // 当前节点对应的区间完全在[l, r]之间
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    } else {  // 一定要分裂
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, d);
        if (r > mid) modify(u << 1 | 1, l, r, d);
        pushup(u);
    }
}

代码

  • C++
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 100010;

int n, m;  // 数列长度、操作个数
int a[N];  // 输入的数组
struct Node {
    int l, r;
    LL sum;  // 如果考虑当前节点及子节点上的所有标记,其区间[l, r]的总和就是sum
    LL add;  // 懒标记,表示需要给以当前节点为根的子树中的每一个节点都加上add这个数(不包含当前节点)
} tr[N * 4];

// 由子节点的信息,来计算父节点的信息
void pushup(int u) {
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

// 把当前父节点的修改信息下传到子节点,也被称为懒标记(延迟标记)
void pushdown(int u) {
    
    auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
    if (root.add) {
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
        root.add = 0;
    }
}

// 创建线段树
void build(int u, int l, int r) {
    
    if (l == r) tr[u] = {l, r, a[l], 0};
    else {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

// 将a[l~r]都加上d
void modify(int u, int l, int r, LL d) {
    
    if (tr[u].l >= l && tr[u].r <= r) {
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    } else {  // 一定要分裂
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, d);
        if (r > mid) modify(u << 1 | 1, l, r, d);
        pushup(u);
    }
}

// 返回a[l~r]元素之和
LL query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    LL sum = 0;
    if (l <= mid) sum += query(u << 1, l, r);
    if (r > mid) sum += query(u << 1 | 1, l, r);
    return sum;
}

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
    build(1, 1, n);
    
    char op[2];
    int l, r, d;
    while (m--) {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C') {
            scanf("%d", &d);
            modify(1, l, r, d);
        } else {
            printf("%lld\n", query(1, l, r));
        }
    }
    
    return 0;
}

AcWing 247. 亚特兰蒂斯

问题描述

分析

  • 本题需要用到扫描线技巧。我们可以统计出所有矩形对应四个顶点的横坐标,过这些横坐标做x的垂线(即扫描线),如下图:

在这里插入图片描述

  • 在沿着扫描线从左向右扫描的过程中,我们可以求解每个h的大小,我们可以这样操作:对于每个矩形与y轴平行的两个边,左边的权值记为+1,右边的权值记为-1(如下图):

在这里插入图片描述

  • 如果当前扫描线上是+1的话,将沿y轴方向的对应区间全部加上1,如果是-1的话,全部加上-1(区间上对应的数表示当前区间被多少个矩形覆盖);为了求出对应的h,我们需要统计出沿y轴的整个区间内大于0的区间长度总和;因此,总结一下,我们存在两个操作:

    (1)对于某个区间加上一个数;

    (2)统计出整个区间中大于0的区间的长度和;

  • 上面的操作对应区间修改、区间查询,因此可以使用线段树来求解,线段树中每个节点存储的信息如下:

struct Node {
    int l, r;  // 区间左右端点,这里是纵坐标离散化后对应的值,是整数
    int cnt;  // 当前区间[l, r]全部都被覆盖的次数
    // 不考虑祖先节点cnt的前提下,只考虑当前节点及子节点,cnt>0的区间总长度,类似于刚才(aAcwing 243)的sum
    double len;
}
  • 此时,本题的基本做法已经讲解完毕,但是我们维护cnt和len比较麻烦,所以我们考虑是否能根据此问题的性质进行优化,最终优化的结果是我们不需要进行pushdown操作,分析如下:

  • 我们注意到本题中的线段树具有如下性质:

    (1)因为我们求扫描线上总高度h,所以我们在查询的时候只会使用到根节点的信息;可以看到query的基本结构如下:

    int query(int u, int l, int r) {
        if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;  // 对于本题,这句话一定会被执行,因此pushdwon执行不到
        
        pushdown(u);
    	// ......
    }
    

    从上面的代码中我们发现,query中的pushdown永远不会被执行到。

    (2)因为矩形存在两条边和y轴平行,因此线段树中所有的操作一定都是成对出现的(所谓成对出现,指的是我们对于某个区间加上一个1,则一定会在之后的操作中再将同样的一个区间减去一个1),且先加后减。可以看到modify的基本结构如下:

    void modify(int u, int l, int r, LL d) {
        if (tr[u].l >= l && tr[u].r <= r) {
            tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
            tr[u].add += d;
        } else {
            pushdown(u);  // 这句话是多余的
            // ......
            pushup(u);
        }
    }
    

    核心:操作的区间是相同的,且先加1后减1。假设modify是给某个区间减1,则这之前这个区间一定都被加过1,对应线段树中的节点(是个集合,记为A)一定有该懒标记,减1的时候一定再会将A中所有的懒标记恢复到加1前的状态,因此,没必要把当前父节点的修改信息下传到子节点,也就是没必要使用pushdown操作。

  • 这一题不用pushdown是因为这个题目十分特殊,可以说这种做法就是针对这个题目的,因此可以单独记下来。

  • 另外这一题中坐标都是小数,我们存储纵坐标的时候需要进行离散化,这里使用vector对纵坐标进行离散化。因为图中有n个矩形,所以平行于y轴的边有 2 n 2n 2n条,因此我们需要存储 2 n 2n 2n个区间,每个区间(平行于y轴的线段)存储一个横坐标,两个纵坐标,并且需要按照横坐标从小到大的顺序排序,存储线段可以使用结构体,如下:

struct Segment
{
    double x, y1, y2;  // 区间的两个端点为(x, y1), (x, y2)
    int k;  // k只能取+1或者-1,表示当前区间的操作是+1还是-1
    bool operator< (const Segment &t) const {  // 让结构体可以从小到大排序
        return x < t.x;
    }
}
  • 我们线段树中维护的内容是一个个的区间,一共 2 n 2n 2n个,因此线段树大小要开到 8 n 8n 8n的大小。
  • 这些区间的纵坐标需要放到ys(是一个vector)中,进行离散化(保序离散化,因为我们要判断区间的包含关系),离散化过程:排序,去重;然后通过lower_bound可以找到每个y在vector中的下标;ys[0]表示最小的一个纵坐标值。
  • 线段树中需要维护 2 n 2n 2n个区间,因此线段树中的每个节点代表一个区间,假设某个区间是是 ( y i , y i + 1 ) (y_i, y_{i+1}) (yi,yi+1),且 y i y_i yi y i + 1 y_{i+1} yi+1之间没有其他的y了,则该区间对应于线段树中的叶节点,则如果 y s [ l 1 ] = y i , y s [ r 1 ] = y i + 1 ys[l1]=y_i, ys[r1]=y_{i+1} ys[l1]=yi,ys[r1]=yi+1,对应的线段树的区间是 [ l 1 , r 1 ] [l1, r1] [l1,r1],另外注意这里的 ( y i , y i + 1 ) (y_i, y_{i+1}) (yi,yi+1)可能不是矩形的某条边,如上图中扫描线 x 8 x_8 x8对应的情况。这里的 x 8 x_8 x8对应线段树中的三个区间(只单纯的考虑后面两个矩形的情况下是3个,否则不是)

在这里插入图片描述

  • 具体来说,上图中存在的最小区间个数为8个,因此线段树中的叶节点也是8个:

在这里插入图片描述

  • 因此线段树中的某个"点"对应于图中是某段区间,比如根节点对应于上图中的区间 [ y 1 , y 8 ] [y_1, y_8] [y1,y8]
  • 线段树中u这个节点表示的是 [ t r [ u ] . l , t r [ u ] . l + 1 ] [tr[u].l, tr[u].l + 1] [tr[u].l,tr[u].l+1] t r [ u ] . l + 1 , t r [ u ] . l + 2 ] tr[u].l + 1, tr[u].l + 2] tr[u].l+1,tr[u].l+2], …, [ t r [ u ] . r , t r [ u ] . r + 1 ] [tr[u].r, tr[u].r + 1] [tr[u].r,tr[u].r+1]这些小区间,这里的 t r [ u ] . l , t r [ u ] . r tr[u].l,tr[u].r tr[u].l,tr[u].r都是对应y离散化后的值。
  • 如上图,这些小区间分别是 [ y 1 , y 2 ] 、 [ y 2 , y 3 ] 、 . . . 、 [ y 8 , y 9 ] [y_1, y_2]、[y_2, y_3]、...、[y_8, y_9] [y1,y2][y2,y3]...[y8,y9],他们的长度组成一个数组a,其中a[0]表示第一个区间的长度,对应离散化区间为[0,1],原区间为[ys[0], ys[0+1]]= [ y 1 , y 2 ] [y_1, y_2] [y1,y2]
  • a[0~2]表示第一个区间的长度,对应离散化区间为[0,1]、[1, 2]、[2, 3],原区间为[ys[0], ys[2+1]]= [ y 1 , y 4 ] [y_1, y_4] [y1,y4]
  • 那么我们的线段树相当于对数组a进行区间修改、区间查询,例如:
  • 当我们要更新 [ y 4 , y 7 ] [y_4, y_7] [y4,y7]这一段区间,相当于更新a[3~5],对应于 a [ f i n d ( y 4 ) − ( f i n d ( y 7 ) ) − 1 ] a[find(y_4) - (find(y_7)) - 1] a[find(y4)(find(y7))1](find函数返回离散化后的值)。

代码

  • C++
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;  // 矩形个数
struct Segment {
    double x, y1, y2;  // 区间的两个端点为(x, y1), (x, y2)
    int k;  // k只能取+1或者-1,表示当前区间的操作是+1还是-1
    bool operator< (const Segment &t) const {
        return x < t.x;
    }
} seg[N * 2];  // 存储区间

struct Node {
    int l, r;  // 纵坐标对应的离散化的值
    int cnt;  // 当前区间[l, r]全部都被覆盖的次数
    // 不考虑祖先节点cnt的前提下,只考虑当前节点及子节点,cnt>0的区间总长度,类似于刚才(aAcwing 243)的sum
    double len;
} tr[N * 8];

vector<double> ys;  // 用于离散化纵坐标

int find(double y) {
    return lower_bound(ys.begin(), ys.end(), y) - ys.begin();
}

void pushup(int u) {
    
    if (tr[u].cnt) {  // 说明节点u对应的区间完全被覆盖
        // 例如tr[u].l = 3, tr[u].r = 5, 对应上面分析的于a[3],a[4],a[5]三个区间
        // 对应离散化区间为[3, 4], [4, 5], [5, 6]
        // 对应上面的[ys[3], ys[4]], [ys[4], ys[5]], [ys[5], ys[6]]
        // 即[y4, y5], [y5, y6], [y6, y7]
        tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
    } else if (tr[u].l != tr[u].r) {  // 没有完全被覆盖,且有子区间
        tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
    } else {  // 没有完全覆盖,且是叶节点
        tr[u].len = 0;
    }
}

void build(int u, int l, int r) {
    
    tr[u] = {l, r, 0, 0};
    if (l != r) {
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    }
}

// 从节点u开始将区间[l, r]加上k
void modify(int u, int l, int r, int k) {
    
    if (tr[u].l >= l && tr[u].r <= r) {
        tr[u].cnt += k;
        pushup(u);
    } else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r , k);
        if (r > mid) modify(u << 1 | 1, l, r, k);
        pushup(u);
    }
}

int main() {
    
    int T = 1;
    while (scanf("%d", &n), n) {
        
        ys.clear();
        for (int i = 0, j = 0; i < n; i++) {
            double x1, y1, x2, y2;
            scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
            seg[j++] = {x1, y1, y2, 1};
            seg[j++] = {x2, y1, y2, -1};
            ys.push_back(y1), ys.push_back(y2);
        }
        
        // 对纵坐标进行离散化
        sort(ys.begin(), ys.end());
        ys.erase(unique(ys.begin(), ys.end()), ys.end());
        
        // ys.size()个纵坐标,ys.size()-1个区间,下标是0~ys.size() - 2
        build(1, 0, ys.size() - 2);
        // 按照横坐标排序(横坐标当成扫描线)
        sort(seg, seg + n * 2);
        
        double res = 0;
        for (int i = 0; i < 2 * n; i++) {
            if (i > 0) res += tr[1].len * (seg[i].x - seg[i - 1].x);
            modify(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].k);
        }
        
        printf("Test case #%d\n", T ++ );
        printf("Total explored area: %.2lf\n\n", res);
    }
    
    return 0;
}

AcWing 1277. 维护序列

问题描述

分析

  • 考虑线段树中的每个节点存储什么信息,如下:
struct Node {
    int l, r;
    int sum;  // 区间总和(已经对p取模)
    int add, mul;  // 懒标记,表示当前节点的子节点的sum值都需要进行sum * mul + add的运算
}
  • 无论是加法还是乘法,我们统一转化为 t × a + b t \times a + b t×a+b的形式

    (1)如果题目是乘以一个数k,则a = k, b = 0;

    (2)如果是加上一个数k,则a = 1, b = k;

  • 对于 t × a + b t \times a + b t×a+b进行同样的操作,即 ( t × a + b ) × c + d (t \times a + b) \times c + d (t×a+b)×c+d,则可以变为 t × a × c + b × c + d t \times a \times c + b \times c + d t×a×c+b×c+d,所 m u l = a × c , a d d = b × c + d mul=a \times c, add=b \times c + d mul=a×c,add=b×c+d

代码

  • C++
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 100010;

int n, p, m;  // 元素个数,取模的数,操作个数
int a[N];
struct Node {
    int l, r;
    int sum, add, mul;
} tr[N * 4];

void pushup(int u) {
    tr[u].sum = (tr[u << 1].sum + tr[u << 1 | 1].sum) % p;
}

void build(int u, int l, int r) {
    
    if (l == r) tr[u] = {l, r, a[l] % p, 0, 0};
    else {
        tr[u] = {l, r, 0, 0, 1};  // sum值会在pushup时被更新
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

// 对节点t进行 (t * mul + add) 的操作
void eval(Node &t, int add, int mul) {
    // sum = sum * mul + len * add;(len为区间长度)
    t.sum = ((LL)t.sum * mul + (LL)(t.r - t.l + 1) * add) % p;
    t.mul = (LL)t.mul * mul % p;
    t.add = ((LL)t.add * mul + add) % p;
}

void pushdown(int u) {
    eval(tr[u << 1], tr[u].add, tr[u].mul);
    eval(tr[u << 1 | 1], tr[u].add, tr[u].mul);
    tr[u].add = 0, tr[u].mul = 1;
}

void modify(int u, int l, int r, int add, int mul) {
    
    if (tr[u].l >= l && tr[u].r <= r) eval(tr[u], add, mul);
    else {
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, add, mul);
        if (r > mid) modify(u << 1 | 1, l, r, add ,mul);
        pushup(u);
    }
}

int query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    int sum = 0;
    if (l <= mid) sum = query(u << 1, l, r);
    if (r > mid) sum = (sum + query(u << 1 | 1, l, r)) % p;
    return sum;
}

int main() {
    
    scanf("%d%d", &n, &p);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
    build(1, 1, n);
    
    scanf("%d", &m);
    while (m--) {
        int t, l, r, d;
        scanf("%d%d%d", &t, &l, &r);
        if (t == 1) {
            scanf("%d", &d);
            modify(1, l, r, 0, d);  // *d + 0
        } else if (t == 2) {
            scanf("%d", &d);
            modify(1, l, r, d, 1);  // *1 + d
        } else {
            printf("%d\n", query(1, l, r));
        }
    }
    
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值