当前位置: 首页 > news >正文

动态开点线段树处理超大范围数据区间问题 - 洛谷P2781

题目描述

有 n 个位置,编号从 1 到 n,初始所有位置的值为 0 。

需要实现以下两种操作,共调用 m 次:

  1. 操作 ( 1 l r v ) :将区间 [l, r] 内的每个数增加 v。
  2. 操作 ( 2 l r ) :返回区间 [l, r] 的累加和。

输入约束

1 ≤ n ≤ 1 0 9 1 \leq n \leq 10^9 1n109
1 ≤ m ≤ 1 0 3 1 \leq m \leq 10^3 1m103

测试链接

https://www.luogu.com.cn/problem/P2781


提示

  • 该题可以帮助学习开点线段树
  • 核心思想是仅在必要时申请空间,避免浪费存储资源。

题解:动态开点线段树


一、题目分析

该题属于 数据结构——动态开点线段树 的应用,涉及 区间修改(加法)与区间求和
由于 ( n ) 的范围高达 ( 10^9 ),无法使用传统数组存储,需要采用动态开点线段树来解决。


二、解题思路

由于 ( n ) 过大,无法直接建立一个大小为 ( n ) 的数组,因此使用 动态开点线段树,即仅在需要时才创建节点,减少空间浪费。

  • 线段树核心操作

    1. add(l, r, v):将区间 ([l, r]) 内的所有数增加 ( v )。
    2. querySum(l, r):查询区间 ([l, r]) 的累加和。
  • 关键技巧

    1. 动态开点:只在访问到某个区间时才创建子节点,避免对所有 ( n ) 个位置预分配存储。
    2. 延迟标记(懒惰更新)
      • 对某个区间进行加法操作时,不直接递归更新所有子节点,而是打上标记,等到查询或细分区间时再下传标记,减少操作次数,提高效率。

三、代码实现

#include <bits/stdc++.h>

#define ll long long
using namespace std;

// 预估节点数:操作次数 * 树高 * 2(控制空间开销)
const int N = 8E4 + 10;  

int cur = 1; // 当前使用的节点索引
struct Node {
    int left, right; // 左右子节点索引
    int add;         // 延迟标记(懒惰标记)
    ll sum;          // 区间和
} tree[N];

// **延迟更新(lazy propagation)**
void lazyAdd(int p, int l, int r, int v) {
    tree[p].sum += 1LL * v * (r - l + 1); // 更新区间总和(避免溢出)
    tree[p].add += v; // 记录懒惰标记
}

// **上推更新**(合并子区间信息)
void pushup(int p) {
    int lp = tree[p].left, rp = tree[p].right;
    tree[p].sum = tree[lp].sum + tree[rp].sum;
}

// **下推懒惰标记**
void pushdown(int p, int l, int r) {
    if (tree[p].add) { // 只有当 add 不为 0 时才需要下推
        int mid = (l + r) >> 1;

        if (tree[p].left == 0) tree[p].left = ++cur;
        lazyAdd(tree[p].left, l, mid, tree[p].add);

        if (tree[p].right == 0) tree[p].right = ++cur;
        lazyAdd(tree[p].right, mid + 1, r, tree[p].add);

        tree[p].add = 0; // 清除当前节点的懒惰标记
    }
}

// **区间加法(更新)**
void add(int p, int l, int r, int jobl, int jobr, int v) {
    if (jobl <= l && r <= jobr) { // 完全覆盖,直接更新
        lazyAdd(p, l, r, v);
    } else { 
        pushdown(p, l, r); // 先下推懒惰标记
        int mid = (l + r) >> 1;
        
        // 左子区间
        if (jobl <= mid) {
            if (tree[p].left == 0) tree[p].left = ++cur;
            add(tree[p].left, l, mid, jobl, jobr, v);
        }
        
        // 右子区间
        if (mid < jobr) {
            if (tree[p].right == 0) tree[p].right = ++cur;
            add(tree[p].right, mid + 1, r, jobl, jobr, v);
        }

        pushup(p); // 更新父节点值
    }
}

// **区间查询(求和)**
ll querySum(int p, int l, int r, int jobl, int jobr) {
    if (jobl <= l && r <= jobr) { // 完全覆盖,直接返回
        return tree[p].sum;
    } else {
        pushdown(p, l, r); // 先下推懒惰标记
        int mid = (l + r) >> 1;
        ll sum = 0;

        if (jobl <= mid && tree[p].left != 0) { 
            sum += querySum(tree[p].left, l, mid, jobl, jobr);
        }
        if (mid < jobr && tree[p].right != 0) { 
            sum += querySum(tree[p].right, mid + 1, r, jobl, jobr);
        }

        return sum;
    }
}

int main() {
    int n, m;
    cin >> n >> m;
    
    for (int i = 0, op, l, r, k; i < m; i++) {
        cin >> op >> l >> r;
        if (op == 1) { // 区间增加
            cin >> k;
            add(1, 1, n, l, r, k);
        } else { // 区间求和
            cout << querySum(1, 1, n, l, r) << endl;
        }
    }
    
    return 0;
}

四、时间 & 空间复杂度分析

时间复杂度

  • add(l, r, v)(区间更新)

    • 最坏情况下,树高为 ( O(log n) )。
    • 由于动态开点,仅访问实际需要的节点,因此时间复杂度接近 ( O(log n) )
  • querySum(l, r)(区间查询)

    • 同样最多访问 ( O(log n) ) 个节点。
    • 时间复杂度 ( O(log n) )
  • 整体复杂度

    • 单次操作:( O(log n) )
    • 总共 ( m ) 次操作:( O(m log n) )

空间复杂度

  • 数组方式:如果直接用数组存储,需要 ( O(n) ) 的空间,但 ( n ) 过大无法接受。
  • 动态开点方式
    • 仅在需要时申请空间。
    • 最坏情况:如果操作覆盖整个 ( n ),最多会创建 ( O(m log n) ) 个节点。
    • 实际情况:一般远小于 ( n )。
    • 空间复杂度:( O(m log n) )

五、总结

  1. 为什么需要动态开点?

    • ( n ) 太大,无法直接用数组存储数据。
    • 通过按需分配节点,减少空间浪费。
  2. 如何优化区间更新?

    • 懒惰标记(Lazy Propagation) 避免不必要的递归更新,提高效率。
  3. 适用场景

    • 适用于 稀疏修改,即数据范围大但实际操作区域较少的情况。
    • 适用于 区间修改 & 区间查询 结合的问题。

本题重点在于 动态开点线段树 + 懒惰标记优化,是处理超大范围数据的关键技巧!

http://www.dtcms.com/a/103212.html

相关文章:

  • Adobe Lightroom 2025安装下载和激活指南
  • Linux常见操作命令(2)
  • 音频进阶学习二十五——脉冲响应不变法实现低通滤波器
  • 【408】26考研-王道计算机408
  • 手工排查后门木马的常用姿势
  • C++之曲线拟合与离散点生成
  • ‌在 Fedora 系统下备份远程 Windows SQL Server 数据库的完整方案
  • Oracle数据库数据编程SQL<3.5 PL/SQL 存储过程(Procedure)>
  • JMeter进行分布式压测
  • 【力扣刷题实战】寻找数组的中心下标
  • Scala基础知识3
  • Kong网关研究
  • Android 中实现一个自定义的 AES 算法
  • 【SPP】深入解析蓝牙 L2CAP 协议在SPP中的互操作性要求 —— 构建可靠的蓝牙串口通信基础
  • CF每日5题(1400)
  • 关于微信小程序云开发,上传数据库时--数据异常问题
  • 从零构建大语言模型全栈开发指南:第四部分:工程实践与部署-4.1.2ONNX格式转换与TensorRT部署
  • 数据库部署在服务器表不存在解决方案
  • LVS负载均衡集群
  • 跨域问题解决
  • 【Linux】进程的详讲(中上)
  • 蓝桥杯数学知识
  • 20250331-智谱-沉思
  • 蓝桥杯备赛之枚举
  • 在Windows Server上安装和配置MinIO对象存储服务
  • PyTorch量化进阶教程:第三章 A 股数据处理与特征工程
  • 以太坊DApp开发脚手架:Scaffold-ETH 2 详细介绍与搭建教程
  • Spring Boot 2.x 与 Nacos 整合规范指南
  • 函数:static和extern
  • 3 通过图形化方式创建helloworld