【算法提高篇】(三)线段树之维护更多的信息:从基础到进阶的灵活运用


目录

前言

一、核心思考:维护多类型信息的三大关键

[二、基础进阶:无修改的多信息维护 ------ 区间最小值查询](#二、基础进阶:无修改的多信息维护 —— 区间最小值查询)

[2.1 例题:忠诚(洛谷 P1816)](#2.1 例题:忠诚(洛谷 P1816))

题目要求

核心分析

[完整 C++ 代码](#完整 C++ 代码)

关键总结

[三、进阶核心:带修改的多信息维护 ------ 懒标记的配合](#三、进阶核心:带修改的多信息维护 —— 懒标记的配合)

[3.1 例题 1:开关(洛谷 P3870 [TJOI2009])](#3.1 例题 1:开关(洛谷 P3870 [TJOI2009]))

题目要求

核心分析

[步骤 1:结构体设计](#步骤 1:结构体设计)

[步骤 2:pushup 实现](#步骤 2:pushup 实现)

[步骤 3:懒标记处理](#步骤 3:懒标记处理)

[步骤 4:修改与查询逻辑](#步骤 4:修改与查询逻辑)

[完整 C++ 代码](#完整 C++ 代码)

关键总结

[3.2 例题 2:贪婪大陆(洛谷 P2184)](#3.2 例题 2:贪婪大陆(洛谷 P2184))

题目要求

核心分析

[步骤 1:结构体设计](#步骤 1:结构体设计)

[步骤 2:pushup 实现](#步骤 2:pushup 实现)

[步骤 3:操作逻辑](#步骤 3:操作逻辑)

[完整 C++ 代码](#完整 C++ 代码)

关键总结

[四、高阶挑战:复杂修改的多信息维护 ------ 等差数列加成](#四、高阶挑战:复杂修改的多信息维护 —— 等差数列加成)

[4.1 例题:无聊的数列(洛谷 P1438)](#4.1 例题:无聊的数列(洛谷 P1438))

题目要求

核心分析

[解法 1:直接维护等差数列信息](#解法 1:直接维护等差数列信息)

[步骤 1:结构体设计](#步骤 1:结构体设计)

[步骤 2:pushup 实现](#步骤 2:pushup 实现)

[步骤 3:懒标记处理(核心难点)](#步骤 3:懒标记处理(核心难点))

[步骤 4:修改与查询逻辑](#步骤 4:修改与查询逻辑)

[解法 1 完整 C++ 代码](#解法 1 完整 C++ 代码)

[解法 2:差分 + 线段树(简化版)](#解法 2:差分 + 线段树(简化版))

[解法 2 核心代码(片段)](#解法 2 核心代码(片段))

关键总结

五、通用解题框架:维护任意信息的线段树模板

[5.1 通用 C++ 模板(含区间修改 + 区间查询)](#5.1 通用 C++ 模板(含区间修改 + 区间查询))

[5.2 模板使用说明](#5.2 模板使用说明)

六、常见易错点总结

总结


前言

在算法学习中,线段树绝对是当之无愧的万能数据结构 ,它凭借分治思想和高效的区间操作能力,能轻松应对单点修改、区间查询、区间修改等经典问题。但实际刷题时,我们遇到的场景远不止维护区间和、最大值这么简单 ------ 可能需要统计区间亮灯数、计算不同地雷种类、维护等差数列加成,甚至是处理 01 序列的连续最长子段。这时候,线段树维护更多类型信息的能力就成了解题的关键。

本文就带大家从基础出发,深入探索线段树如何灵活维护各类复杂信息,从结构体设计、懒标记处理到具体例题实战,一步步拆解核心思路,让你彻底掌握线段树的进阶用法!下面就让我们正式开始吧!


一、核心思考:维护多类型信息的三大关键

线段树的本质是用二叉树维护区间信息,基础的区间和、最大值维护,只需要在结构体中定义单个变量,配合简单的pushup整合左右孩子信息即可。但要维护更复杂的信息,核心要解决三个问题,这也是贯穿所有进阶题型的通用思路:

  1. 结构体该存什么? :根据问题需求,确定需要维护的所有区间信息,比如统计连续 1 的长度,需要维护区间最长连续 1、左端点开始最长连续 1、右端点结束最长连续 1等;
  2. 父节点信息如何来? :实现pushup函数,明确如何通过左右孩子的信息,推导出父节点的完整信息,这是分治思想的核心体现;
  3. 懒标记该怎么发? :如果涉及区间修改,需要设计对应的懒标记,明确标记的含义、如何下放(pushdown)、如何更新子节点的信息,确保修改操作的延迟执行不影响结果。

简单来说,只要想清楚这三个问题,无论多复杂的信息维护,都能套上线段树的框架解决。接下来,我们通过经典例题,从易到难拆解这些思路的实际应用。

二、基础进阶:无修改的多信息维护 ------ 区间最小值查询

先从无修改操作 的场景入手,帮大家建立 "按需定义结构体" 的思维。这类问题不需要懒标记,核心是设计结构体和实现pushup,属于维护多信息的入门题型。

2.1 例题:忠诚(洛谷 P1816)

题目链接:https://www.luogu.com.cn/problem/P1816

题目要求

给定 m 笔账目,多次查询区间[a,b]内的最小值,无修改操作。

核心分析

  • 结构体设计 :需要维护区间的左右边界l/r,以及当前区间的最小值min
  • pushup 实现:父节点的最小值 = 左孩子最小值 和 右孩子最小值 中的较小值;
  • 查询逻辑:基础的区间拆分拼凑,完全覆盖则返回最小值,否则递归左右孩子取最小。

完整 C++ 代码

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

#define lc p << 1  // 左孩子节点编号
#define rc p << 1 | 1  // 右孩子节点编号
const int N = 1e5 + 10;
const int INF = 1e9;

int n, m;
int a[N];  // 原始数组

// 线段树节点结构体:维护区间左右边界 + 区间最小值
struct node
{
    int l, r, min;
}tr[N << 2];  // 线段树空间开4倍

// 构建线段树
void build(int p, int l, int r)
{
    tr[p] = {l, r, a[l]};
    if (l == r) return;  // 叶子节点,直接返回
    int mid = (l + r) >> 1;
    build(lc, l, mid);   // 构建左子树
    build(rc, mid + 1, r);  // 构建右子树
    // 整合左右孩子信息:父节点最小值 = 左右孩子最小值的较小值
    tr[p].min = min(tr[lc].min, tr[rc].min);
}

// 区间查询最小值:查询[x,y]的最小值
int query(int p, int x, int y)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y) return tr[p].min;  // 完全覆盖,直接返回
    int mid = (l + r) >> 1;
    int res = INF;
    if (x <= mid) res = min(res, query(lc, x, y));  // 左孩子有重叠,递归查询
    if (y > mid) res = min(res, query(rc, x, y));   // 右孩子有重叠,递归查询
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);  // 根节点编号为1,维护[1,n]
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        cout << query(1, a, b) << " ";
    }
    return 0;
}

关键总结

这道题是基础的多信息维护入门,虽然只维护了一个 "最小值",但核心思路是按需定义结构体 。如果题目要求查询区间最大值 + 最小值,只需要在结构体中增加max变量,pushup时同时更新minmax即可,框架完全不变。

三、进阶核心:带修改的多信息维护 ------ 懒标记的配合

实际刷题中,绝大多数进阶题型都包含区间修改 + 区间查询 ,此时需要在 "结构体设计 + pushup" 的基础上,增加懒标记的设计和 pushdown 的实现。这也是线段树维护多信息的难点,我们通过两个经典例题,拆解不同类型懒标记的处理思路。

3.1 例题 1:开关(洛谷 P3870 [TJOI2009])

题目链接:https://www.luogu.com.cn/problem/P3870

题目要求

有 n 盏初始关闭的灯,支持两种操作:

  1. 区间[a,b]翻转灯的状态(开→关,关→开);
  2. 查询区间[a,b]内打开的灯的数量。

核心分析

这道题需要维护区间亮灯数 ,同时处理区间翻转 的修改操作,核心是设计翻转懒标记,并明确标记下放时如何更新子节点的亮灯数。

步骤 1:结构体设计

需要维护的信息:

  • 区间左右边界l/r
  • 区间亮灯数量sum
  • **懒标记cnt:**记录区间翻转的次数(奇数表示需要翻转,偶数表示无需翻转)。
步骤 2:pushup 实现

父节点的亮灯数 = 左孩子亮灯数 + 右孩子亮灯数,这是基础的求和整合,难度不大。

步骤 3:懒标记处理
  • lazy 函数:接收翻转次数,更新当前节点的亮灯数(翻转后亮灯数 = 区间长度 - 原亮灯数),并累加懒标记;
  • pushdown 函数:将当前节点的翻转标记下放给左右孩子,然后清空当前节点的标记,确保后续递归时子节点的信息是最新的。
步骤 4:修改与查询逻辑
  • 修改 :完全覆盖则执行lazy翻转,否则先pushdown下放标记,再递归左右孩子,最后pushup整合信息;
  • 查询 :完全覆盖则返回亮灯数,否则先pushdown,再递归左右孩子求和。

完整 C++ 代码

cpp 复制代码
#include <iostream>
using namespace std;

#define lc p << 1
#define rc p << 1 | 1
const int N = 1e5 + 10;

int n, m;
// 线段树节点:l/r=区间边界,sum=亮灯数,cnt=翻转懒标记(翻转次数)
struct node
{
    int l, r, sum, cnt;
}tr[N << 2];

// 懒标记更新:当前节点翻转cnt次
void lazy(int p, int cnt)
{
    if (cnt % 2 == 1)  // 奇数翻转才会改变状态
        tr[p].sum = tr[p].r - tr[p].l + 1 - tr[p].sum;
    tr[p].cnt += cnt;  // 累加翻转次数
}

// 整合左右孩子信息
void pushup(int p)
{
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

// 下放懒标记给左右孩子
void pushdown(int p)
{
    if (tr[p].cnt == 0) return;  // 无标记,无需下放
    lazy(lc, tr[p].cnt);
    lazy(rc, tr[p].cnt);
    tr[p].cnt = 0;  // 清空当前节点标记
}

// 构建线段树:初始所有灯关闭,sum=0,cnt=0
void build(int p, int l, int r)
{
    tr[p] = {l, r, 0, 0};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(p);
}

// 区间修改:翻转[x,y]的灯状态
void modify(int p, int x, int y)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y)
    {
        lazy(p, 1);  // 翻转1次
        return;
    }
    pushdown(p);  // 先下放标记
    int mid = (l + r) >> 1;
    if (x <= mid) modify(lc, x, y);
    if (y > mid) modify(rc, x, y);
    pushup(p);  // 整合左右孩子信息
}

// 区间查询:查询[x,y]的亮灯数
int query(int p, int x, int y)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y) return tr[p].sum;
    pushdown(p);  // 先下放标记
    int mid = (l + r) >> 1;
    int res = 0;
    if (x <= mid) res += query(lc, x, y);
    if (y > mid) res += query(rc, x, y);
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    build(1, 1, n);
    while (m--)
    {
        int op, a, b;
        cin >> op >> a >> b;
        if (op == 0) modify(1, a, b);  // 翻转
        else cout << query(1, a, b) << endl;  // 查询
    }
    return 0;
}

关键总结

这道题的懒标记是计数型 ,核心是根据标记的奇偶性 更新信息。这类懒标记的特点是:标记的累加会改变操作的效果,需要根据实际逻辑判断是否执行更新。同时,pushdown 的时机是关键 ------ 所有递归操作(modify/query)前,只要不是叶子节点,都要先判断是否有懒标记,有则下放,确保子节点信息准确。

3.2 例题 2:贪婪大陆(洛谷 P2184)

题目链接:https://www.luogu.com.cn/problem/P2184

题目要求

给定长度为 n 的战壕,支持两种操作:

  1. 区间[l,r]布置一种新地雷(每种地雷唯一);
  2. 查询区间[l,r]内有多少种不同的地雷。

核心分析

这道题的难点在于如何统计区间内的地雷种类 ,直接维护种类数会非常复杂,需要通过转化问题来简化信息维护:

核心转化 :对于一次布置[L,R]的地雷,相当于在L位置加一个起点标记 ,在R位置加一个终点标记 。查询[l,r]的地雷种类数 = [1,r]的起点数 - [1,l-1]的终点数。

通过这个转化,问题就变成了维护区间内的起点数和终点数,属于基础的区间查询 + 单点修改,无需复杂的懒标记,大大降低了难度。

步骤 1:结构体设计

需要维护区间左右边界l/r,以及两个计数:cnt[0](起点数)、cnt[1](终点数)。

步骤 2:pushup 实现

父节点的起点数 = 左孩子起点数 + 右孩子起点数,终点数同理,简单的求和整合。

步骤 3:操作逻辑
  • 布置地雷 :对L位置执行单点修改,cnt[0]++;对R位置执行单点修改,cnt[1]++
  • 查询种类 :计算**query(1,1,r,0) - query(1,1,l-1,1)**即可。

完整 C++ 代码

cpp 复制代码
#include <iostream>
using namespace std;

#define lc p << 1
#define rc p << 1 | 1
const int N = 1e5 + 10;

int n, m;
// 线段树节点:cnt[0]=起点数,cnt[1]=终点数
struct node
{
    int l, r, cnt[2];
}tr[N << 2];

// 整合左右孩子信息
void pushup(int p)
{
    tr[p].cnt[0] = tr[lc].cnt[0] + tr[rc].cnt[0];
    tr[p].cnt[1] = tr[lc].cnt[1] + tr[rc].cnt[1];
}

// 构建线段树:初始起点数和终点数都为0
void build(int p, int l, int r)
{
    tr[p] = {l, r, 0, 0};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(p);
}

// 单点修改:在x位置,k类型(0=起点,1=终点)计数+1
void modify(int p, int x, int k)
{
    int l = tr[p].l, r = tr[p].r;
    if (l == x && r == x)
    {
        tr[p].cnt[k]++;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) modify(lc, x, k);
    else modify(rc, x, k);
    pushup(p);
}

// 区间查询:查询[x,y]内k类型的计数
int query(int p, int x, int y, int k)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y) return tr[p].cnt[k];
    int mid = (l + r) >> 1;
    int res = 0;
    if (x <= mid) res += query(lc, x, y, k);
    if (y > mid) res += query(rc, x, y, k);
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    build(1, 1, n);
    while (m--)
    {
        int op, l, r;
        cin >> op >> l >> r;
        if (op == 1)
        {
            // 布置地雷:l加起点,r加终点
            modify(1, l, 0);
            modify(1, r, 1);
        }
        else
        {
            // 查询种类:[1,r]起点数 - [1,l-1]终点数
            int ans = query(1, 1, r, 0) - query(1, 1, l - 1, 1);
            cout << ans << endl;
        }
    }
    return 0;
}

关键总结

这道题的核心不是线段树的语法,而是问题的转化能力 。很多时候,直接维护题目要求的信息会非常复杂,需要通过数学推导、逻辑转化,将问题转化为线段树能轻松维护的信息(如计数、求和、最值)。这也是算法学习的核心 ------先转化问题,再选择数据结构

四、高阶挑战:复杂修改的多信息维护 ------ 等差数列加成

前面的例题都是简单的区间修改(翻转、单点计数),接下来的无聊的数列(洛谷 P1438) 涉及区间等差数列加成 ,属于更复杂的区间修改,需要设计维护等差数列信息的懒标记 ,同时处理标记下放时的首项偏移问题,是维护多信息的高阶题型。

4.1 例题:无聊的数列(洛谷 P1438)

题目链接:https://www.luogu.com.cn/problem/P1438

题目要求

维护一个数列,支持两种操作:

  1. 区间[l,r]加上一个等差数列,首项为 K,公差为 D(即a[l]+Ka[l+1]+K+D,...,a[r]+K+(r-l)*D);
  2. 单点查询a[p]的值。

核心分析

这道题有两种解法,我们重点讲解直接维护等差数列信息 的解法,更能体现线段树维护复杂信息的能力;同时补充差分 + 线段树的解法,供大家对比学习。

解法 1:直接维护等差数列信息

步骤 1:结构体设计

需要维护的信息:

  • 区间左右边界l/r
  • 区间和sum:用于单点查询(叶子节点的 sum 就是对应位置的值);
  • 懒标记kd:分别表示当前区间需要加上的等差数列的首项公差
步骤 2:pushup 实现

父节点的区间和 = 左孩子区间和 + 右孩子区间和,基础求和逻辑。

步骤 3:懒标记处理(核心难点)
  • lazy 函数 :接收等差数列的首项k和公差d,根据等差数列求和公式更新当前节点的区间和:sum += (首项+末项)项数/2,其中末项 =k + (区间长度-1)d项数 =r-l+1;同时累加懒标记kd
  • pushdown 函数 :下放懒标记时,右孩子的首项需要偏移 ------ 左孩子的首项就是父节点的k,公差d;右孩子的*首项 =k + (右孩子左端点 - 父节点左端点)d,公差仍为d。下放后清空父节点的懒标记。
步骤 4:修改与查询逻辑
  • 修改 :完全覆盖则执行lazy更新,否则先pushdown,再递归左右孩子,最后pushup
  • 查询 :单点查询,递归到叶子节点返回sum即可,过程中需要pushdown下放标记。

解法 1 完整 C++ 代码

cpp 复制代码
#include <iostream>
using namespace std;

#define lc p << 1
#define rc p << 1 | 1
typedef long long LL;  // 防止溢出
const int N = 1e5 + 10;

int n, m;
int a[N];
// 线段树节点:sum=区间和,k=等差数列首项懒标记,d=公差懒标记
struct node
{
    int l, r;
    LL sum, k, d;
}tr[N << 2];

// 整合左右孩子信息
void pushup(int p)
{
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

// 懒标记更新:当前区间加上首项k,公差d的等差数列
void lazy(int p, LL k, LL d)
{
    int l = tr[p].l, r = tr[p].r;
    int len = r - l + 1;
    LL last = k + (len - 1) * d;  // 等差数列末项
    tr[p].sum += (k + last) * len / 2;  // 等差数列求和
    tr[p].k += k;
    tr[p].d += d;
}

// 下放懒标记:核心处理右孩子的首项偏移
void pushdown(int p)
{
    if (tr[p].k == 0 && tr[p].d == 0) return;
    int l = tr[p].l, r = tr[p].r;
    int mid = (l + r) >> 1;
    // 左孩子:首项=tr[p].k,公差=tr[p].d
    lazy(lc, tr[p].k, tr[p].d);
    // 右孩子:首项偏移 = tr[p].k + (mid+1 - l)*tr[p].d
    lazy(rc, tr[p].k + (mid + 1 - l) * tr[p].d, tr[p].d);
    // 清空父节点标记
    tr[p].k = 0;
    tr[p].d = 0;
}

// 构建线段树
void build(int p, int l, int r)
{
    tr[p] = {l, r, a[l], 0, 0};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(p);
}

// 区间修改:[x,y]加上首项k,公差d的等差数列
void modify(int p, int x, int y, LL k, LL d)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y)
    {
        // 当前区间的首项需要偏移:k + (l - x)*d
        lazy(p, k + (l - x) * d, d);
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid) modify(lc, x, y, k, d);
    if (y > mid) modify(rc, x, y, k, d);
    pushup(p);
}

// 单点查询:查询x位置的值
LL query(int p, int x)
{
    int l = tr[p].l, r = tr[p].r;
    if (l == x && r == x) return tr[p].sum;
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid) return query(lc, x);
    else return query(rc, x);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);
    while (m--)
    {
        int op;
        cin >> op;
        if (op == 1)
        {
            int l, r;
            LL k, d;
            cin >> l >> r >> k >> d;
            modify(1, l, r, k, d);
        }
        else
        {
            int p;
            cin >> p;
            cout << query(1, p) << endl;
        }
    }
    return 0;
}

解法 2:差分 + 线段树(简化版)

核心转化 :将区间等差数列加成,转化为三次区间单点修改,用基础的区间和线段树即可实现,无需复杂的懒标记。

  • 设差分数组为diff,原始数组a[i] = diff[1]+diff[2]+...+diff[i]
  • 区间[l,r]加首项 K、公差 D 的等差数列,等价于:
    1. diff[l] += K
    2. diff[l+1...r] += D
    3. diff[r+1] -= (K + (r-l)*D)
  • 单点查询a[p],等价于查询差分数组[1,p]的区间和。

这种解法通过差分将复杂的等差数列修改,转化为线段树的基础操作,代码更简单,适合对懒标记掌握不熟练的同学。

解法 2 核心代码(片段)

cpp 复制代码
// 差分数组的线段树,维护区间和+区间加懒标记(基础版)
void modify(int p, int x, int y, LL add)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y)
    {
        tr[p].sum += add * (r - l + 1);
        tr[p].add += add;
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid) modify(lc, x, y, add);
    if (y > mid) modify(rc, x, y, add);
    pushup(p);
}

// 主函数中的修改操作
if (op == 1)
{
    int l, r;
    LL k, d;
    cin >> l >> r >> k >> d;
    modify(1, l, l, k);  // 第一步
    if (l+1 <= r) modify(1, l+1, r, d);  // 第二步
    if (r+1 <= n) modify(1, r+1, r+1, -(k + (r-l)*d));  // 第三步
}

关键总结

这道题的两种解法体现了线段树的灵活性

  1. 直接维护复杂信息: 适合锻炼懒标记的设计和处理能力,核心是解决标记下放的偏移问题
  2. **转化为基础问题:**通过差分、数学推导等方式,将复杂操作转化为线段树的基础操作,降低编码难度。

实际刷题中,建议先尝试转化问题,如果转化不了,再考虑直接维护复杂信息。

五、通用解题框架:维护任意信息的线段树模板

通过上面的例题,我们可以总结出维护任意类型信息的线段树通用框架 ,无论题目要求维护什么信息,都可以套这个框架,只需要根据需求修改结构体、pushup、lazy、pushdown四个部分,其余代码基本不变。

5.1 通用 C++ 模板(含区间修改 + 区间查询)

cpp 复制代码
#include <iostream>
using namespace std;
#define lc p << 1
#define rc p << 1 | 1
typedef long long LL;
const int N = 1e5 + 10;

int n, m;
int a[N];  // 原始数组

// 步骤1:根据问题需求,定义结构体(维护的信息+懒标记)
struct node
{
    int l, r;
    // ******** 自定义维护的信息 ********
    LL sum;  // 示例:区间和
    // ******** 自定义懒标记 ********
    LL add;  // 示例:区间加懒标记
}tr[N << 2];

// 步骤2:实现pushup------整合左右孩子信息到父节点
void pushup(int p)
{
    // ******** 自定义整合逻辑 ********
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

// 步骤3:实现lazy------用懒标记更新当前节点信息
void lazy(int p, LL val)
{
    // ******** 自定义懒标记更新逻辑 ********
    int len = tr[p].r - tr[p].l + 1;
    tr[p].sum += val * len;
    tr[p].add += val;
}

// 步骤4:实现pushdown------下放懒标记给左右孩子
void pushdown(int p)
{
    // ******** 自定义标记下放逻辑 ********
    if (tr[p].add == 0) return;
    lazy(lc, tr[p].add);
    lazy(rc, tr[p].add);
    tr[p].add = 0;  // 清空当前节点标记
}

// 构建线段树(基本不变)
void build(int p, int l, int r)
{
    // ******** 初始化结构体 ********
    tr[p] = {l, r, a[l], 0};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(p);
}

// 区间修改(基本不变,仅修改传入参数)
void modify(int p, int x, int y, LL val)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y)
    {
        lazy(p, val);
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid) modify(lc, x, y, val);
    if (y > mid) modify(rc, x, y, val);
    pushup(p);
}

// 区间查询(基本不变,仅修改返回值和查询逻辑)
LL query(int p, int x, int y)
{
    int l = tr[p].l, r = tr[p].r;
    if (x <= l && r <= y)
    {
        // ******** 自定义返回值 ********
        return tr[p].sum;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    LL res = 0;  // ******** 初始化查询结果 ********
    if (x <= mid) res += query(lc, x, y);  // ******** 自定义查询逻辑 ********
    if (y > mid) res += query(rc, x, y);  // ******** 自定义查询逻辑 ********
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);
    while (m--)
    {
        int op, x, y;
        LL val;
        cin >> op >> x >> y;
        if (op == 1)
        {
            cin >> val;
            modify(1, x, y, val);
        }
        else
        {
            cout << query(1, x, y) << endl;
        }
    }
    return 0;
}

5.2 模板使用说明

  1. 结构体 :删除示例的sumadd,添加题目需要维护的信息(如min/maxlmax/rmaxcnt[2])和对应的懒标记(如cntk/drev);
  2. pushup:根据左右孩子的信息,写出父节点信息的推导公式,这是最核心的部分;
  3. lazy:根据懒标记的含义,写出当前节点信息的更新逻辑,注意累加 / 覆盖懒标记;
  4. pushdown:将当前节点的懒标记下放给左右孩子,更新孩子的信息,然后清空当前节点的标记;
  5. build/modify/query:除了初始化、参数传入、查询结果整合外,其余代码基本不变,只需根据需求微调。

六、常见易错点总结

在维护多类型信息的线段树编码中,新手很容易出现各种错误,这里总结了五大高频易错点,帮大家避坑:

  1. 懒标记只存不更:只修改了懒标记,但没有更新当前节点的信息,导致后续查询结果错误;
  2. pushdown 时机缺失:在 modify/query 前没有执行 pushdown,子节点的信息还是旧的,查询 / 修改结果错误;
  3. pushdown 后未清空标记:下放标记后没有将父节点的懒标记置为初始值,导致标记被重复下放,多次更新;
  4. 空间开太小 :线段树的空间必须开原始数组的 4 倍,否则会出现数组越界,程序崩溃;
  5. 数据溢出 :维护区间和、等差数列和等信息时,一定要用long long类型,避免 int 溢出。

总结

线段树作为算法竞赛中的 "万能工具",掌握了其维护多类型信息的能力,就能应对 80% 以上的区间操作问题。希望本文能帮大家建立清晰的思路,在刷题中逐步吃透线段树的进阶用法!

相关推荐
ShineWinsu2 小时前
对于C++中list的详细介绍
开发语言·数据结构·c++·算法·面试·stl·list
mjhcsp2 小时前
C++Lyndon 分解超详解析
c++·算法·lyndon
Mr_health2 小时前
leetcode:组合排列系列
算法·leetcode·职场和发展
冬夜戏雪2 小时前
Leetcode 颠倒二进制位/二进制求和
java·数据结构·算法
俩娃妈教编程2 小时前
2023 年 09 月 二级真题(1)--小杨的 X 字矩阵
数据结构·c++·算法·双层循环
铸人2 小时前
再论自然数全加和 - 欧拉伽马常数4
算法
prince_zxill2 小时前
探索Nautilus Trader:高性能算法交易平台与事件驱动回测引擎的全面指南
算法
进击的荆棘2 小时前
算法——二分查找
c++·算法·leetcode
识君啊3 小时前
Java 滑动窗口 - 附LeetCode经典题解
java·算法·leetcode·滑动窗口