线段树的又一篇详解

概念

线段树,它的每一个节点都保存了一条线段,可以高效地解决连续区间的动态查询问题,能基本保持每个操作的复杂度为O(logn)。

定义

我们定义一个结构体p来存储这个树

struct P {
    int left, right, mid, value, lazy;
    //lazy用于后期的lazy tag,前半篇文章不会讲到
} p[MAXN * 8];
//MAXN表示最大的原数组长度
//一般开出来的数组不会超过原长度的4倍
//由于后期由于要用上lazy tag所以开8倍

建树

我们定义一个函数build来完成建树操作(将每一个节点的值都设为0

void build();

分别用变量root、left、right来表示当前要build的节点、这个节点的左端点和右端点,即

void build(int root, int left, int right);

调用时,即

build(1, 1, n);
//n即数组长度

然后就开始写这个函数

void build(int root, int left, int right) {
    p[root].left = left;
    p[root].right = right;
    p[root].mid = (left + right) / 2; //也可以写成(left + right) >> 1
    p[root].value = 0; //给当前节点赋值
    if (p[root].left == p[root].right) return ; //结束递归
    int mid = (left + right) / 2;
    build(root * 2, left, mid);
    build(root * 2 + 1, mid + 1, right);
    //上面是为了方便理解,为了效率也可以这样写
    //int mid = (left + right) >> 1;
    //build(root << 1, left, mid);
    //build((root << 1) + 1, mid + 1, right);
}

单点修改

我们现在打一个修改单一变量的代码,相信会了这个以后,区间修改也不是问题(至少对我来说是这样)

void add(int root, int k, int value) {
    //root表示当前修改的节点,k表示当前修改的变量(在原数组中)的下标,value表示增加的值
    p[root].value += value; //增加当前节点的值
    if (p[root].left == p[root].right) return ; //结束递归
    if (k <= p[root].mid) add(root * 2, k, value); //在左孩子这边
    else add(root * 2 + 1, k, value); //在右孩子这边
    //也可以写成
    //if (k <= p[root].mid) add(root >> 1, k, value);
    //else add((root >> 1) + 1, k, value);
}

调用的时候,只需要

add(1, k, value);
//k就是下标,value就是要增加的值
//由于这个点肯定包含到1-n这一条线段中,所以直接从第一个点开始增加即可

对于Luogu的线段树模板题一,需要对数组初始化,可以这么写

int n, a[MAXN];
scanf("%d", &n);
build(1, 1, n);
for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
    add(1, i, a[i]);
}

区间修改

看懂单点修改后,区间修改应该很简单了吧
注意区间修改的话要添加的值也应该是一个区间的

void add(int root, int left, int right, int value) {
    //root表示当前修改的节点,left表示左边,right表示右边,value表示增加的值
    p[root].value += value * (right - left + 1); //增加当前节点的值
    if (p[root].left == p[root].right) return ; //结束递归
    if (right <= p[root].mid) add(root * 2, left, right, value); //在左孩子这边
    else if (left > p[root].mid) add(root * 2 + 1, left, right, value); //在右孩子这边
    else {
        add(root * 2, left, p[root].mid, value);
        add(root * 2 + 1, p[root].mid + 1, right, value);
        //分段
    }
    //优化方式这里不放了
}

(看懂了吗?)

区间查询

int find(int root, int left, int right) {
    //定义同上
    if (left == p[root].left && right == p[root].right) return p[root].value; //找到目标,结束递归
    if (right <= p[root].mid) return find(root * 2, left, right); //在左孩子这边
    else if (left > p[root].mid) return find(root * 2 + 1, left, right); //在右孩子这边
    else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
    //进一步递归,优化同样不打了
} //分段

调用的时候只要

printf("%d\n", find(1, k, k)); //单点查询
printf("%d\n", find(1, left, right)); //区间查询

练手(一)

那么你已经学会了基本的一些操作,打道Luogu的模板题尝试一下吧
链接地址:LuoguP3372线段树(一)

题目描述

如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.求出某区间每一个数的和

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式

输出包含若干行整数,即为所有操作2的结果。

输入样例
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出样例
11
8
20
代码
#include <bits/stdc++.h>
#define MAXN 100001
using namespace std;
int n, m, a[MAXN];
struct P {
    int left, right, mid, value;
} p[MAXN * 8]; //定义
void build(int root, int left, int right) {
    p[root].left = left;
    p[root].right = right;
    p[root].value = 0;
    if (left == right) return ;
    int mid = p[root].mid = (left + right) >> 1;
    build(root << 1, left, mid);
    build((root << 1) + 1, mid + 1, right);
} //建树
void add(int root, int left, int right, int value) {
    p[root].value += value * (right - left + 1);
    if (p[root].left == p[root].right) return ;
    if (right <= p[root].mid) add(root * 2, left, right, value);
    else if (left > p[root].mid) add(root * 2 + 1, left, right, value);
    else {
        add(root * 2, left, p[root].mid, value);
        add(root * 2 + 1, p[root].mid + 1, right, value);
    }
} //修改
int find(int root, int left, int right) {
    if (left == p[root].left && right == p[root].right) return p[root].value;
    if (right <= p[root].mid) return find(root * 2, left, right);
    else if (left > p[root].mid) return find(root * 2 + 1, left, right);
    else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
} //查询
int main() {
    scanf("%d%d", &n, &m);
    build(1, 1, n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        add(1, i, i, a[i]);
    } //建树
    int k, x, y, z;
    for (int i = 1; i <= m; i++) {
        scanf("%d", &k);
        if (k == 1) {
            scanf("%d%d%d", &x, &y, &z);
            add(1, x, y, z); //修改
        } else {
            scanf("%d%d", &x, &y);
            printf("%d\n", find(1, x, y)); //查询
        }
    }
    return 0;
}

提交上后TLE70分,这又是为什么呢?

线段树优化 - lazy tag

如果你自己思考一下可以发现,这样的线段树处理其实是非常浪费时间的。因为每一次的修改操作都要一层一层地执行下去,但很多时候这个修改是不会被使用到的。所以我们就需要lazy tag来进行优化。
如果你已经一路看到这里,建议你先喝杯咖啡放松一下(不要问我为什么)。

原理

lazy tag的原理其实很好理解。本来线段树的修改需要递归到最后一层,现在如果发现当前节点所表示的一整条线段都需要修改,就打上lazy tag,放在那里,等到查询到的时候再计算

打标记(pushdown)操作
void pushdown(int root, int value) {
    //由于是整条线段所以没有必要传递左指针和右指针
    p[root].lazy += value; //注意不是=(等于)
    p[root].value += value * (p[root].right - p[root].left + 1);
}
区间修改(带lazy tag
void add(int root, int left, int right, int value) {
    //root表示当前修改的节点,left表示左边,right表示右边,value表示增加的值
    //原代码:p[root].value += value * (right - left + 1); //增加当前节点的值
    //原代码:if (p[root].left == p[root].right) return ; //结束递归
    if (p[root].left == left && p[root].right == right) {
        //整条线段都需要覆盖
        pushdown(root, value); //去打lazy tag
        return ;
    }
    p[root].value += value * (right - left + 1);
    //下面和原来一样
    if (right <= p[root].mid) add(root * 2, left, right, value); //在左孩子这边
    else if (left > p[root].mid) add(root * 2 + 1, left, right, value); //在右孩子这边
    else {
        add(root * 2, left, p[root].mid, value);
        add(root * 2 + 1, p[root].mid + 1, right, value);
        //分段
    }
}

调用方法和原来一样

区间查询(带lazy tag
int find(int root, int left, int right) {
    if (p[root].lazy) {
        //发现lazy tag
        pushdown(root * 2, p[root].lazy); //给左孩子打上
        pushdown(root * 2 + 1, p[root].lazy); //给右孩子打上
        p[root].lazy = 0; //清除lazy tag
    }
    if (left == p[root].left && right == p[root].right) return p[root].value; //找到目标,结束递归
    if (right <= p[root].mid) return find(root * 2, left, right); //在左孩子这边
    else if (left > p[root].mid) return find(root * 2 + 1, left, right); //在右孩子这边
    else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right); //分段
}
修改后的代码
#include <bits/stdc++.h>
#define MAXN 100001
using namespace std;
int n, m;
long long a[MAXN];
struct P {
    int left, right, mid;
    long long lazy, value;
} p[MAXN * 8]; //定义
void build(int root, int left, int right) {
    p[root].left = left;
    p[root].right = right;
    p[root].value = 0;
    if (left == right) return ;
    int mid = p[root].mid = (left + right) >> 1;
    build(root << 1, left, mid);
    build((root << 1) + 1, mid + 1, right);
} //建树
void pushdown(int root, long long value) {
    //由于是整条线段所以没有必要传递左指针和右指针
    p[root].lazy += value; //注意不是=(等于)
    p[root].value += value * (p[root].right - p[root].left + 1);
}
void add(int root, int left, int right, long long value) {
    if (p[root].left == left && p[root].right == right) {
        //整条线段都需要覆盖
        pushdown(root, value); //去打lazy tag
        return ;
    }
    p[root].value += value * (right - left + 1);
    if (right <= p[root].mid) add(root * 2, left, right, value);
    else if (left > p[root].mid) add(root * 2 + 1, left, right, value);
    else {
        add(root * 2, left, p[root].mid, value);
        add(root * 2 + 1, p[root].mid + 1, right, value);
    }
} //修改
long long find(int root, int left, int right) {
    if (p[root].lazy) {
        //发现lazy tag
        pushdown(root * 2, p[root].lazy); //给左孩子打上
        pushdown(root * 2 + 1, p[root].lazy); //给右孩子打上
        p[root].lazy = 0; //清除lazy tag
    }
    if (left == p[root].left && right == p[root].right) return p[root].value;
    if (right <= p[root].mid) return find(root * 2, left, right);
    else if (left > p[root].mid) return find(root * 2 + 1, left, right);
    else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
} //查询
int main() {
    scanf("%d%d", &n, &m);
    build(1, 1, n);
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        add(1, i, i, a[i]);
    } //建树
    int k, x, y, z;
    for (int i = 1; i <= m; i++) {
        scanf("%d", &k);
        if (k == 1) {
            scanf("%d%d%d", &x, &y, &z);
            add(1, x, y, z);
        } else {
            scanf("%d%d", &x, &y);
            printf("%lld\n", find(1, x, y));
        }
    }
    return 0;
}

AC100分 (需要开long long)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值