P3372 【模板】线段树 1 线段树/分块解法

线段树模板题解析

解题思路

这道题是一个典型的线段树应用问题,需要高效地处理区间更新(区间加值)和区间查询(区间求和)两种操作。线段树是一种二叉树结构,能够以O(logN)的时间复杂度完成这两种操作。

解题步骤:

  1. 构建线段树:将原始数组构建成线段树,每个节点存储对应区间的和

  2. 区间更新:使用懒标记(lazy tag)技术延迟更新,只在需要时才将标记下推,提高效率

  3. 区间查询:查询时同样需要处理懒标记,确保数据正确性

代码注释

#include<bits/stdc++.h>
#define f(i,s,e) for(int i = s; i <= e; i++) // 定义循环宏,从s到e
#define lc rt * 2     // 左子节点索引
#define rc rt * 2 + 1   // 右子节点索引
#define lson lc,l,mid   // 左子树参数
#define rson rc,mid +1,r // 右子树参数
#define ll long long    // 定义long long类型别名
using namespace std;

const int N = 2e5+10, inf = 0x3f3f3f3f;

// 线段树节点结构体
struct node{
    ll sum, lazy; // sum存储区间和,lazy是懒标记
};

node t[N * 4];  // 线段树数组,大小是4倍原始数组
ll n, m, a[N];  // n-数组长度,m-操作次数,a-原始数组

// 更新父节点的sum值为左右子节点sum之和
void pushup(int rt) {
    t[rt].sum = t[lc].sum + t[rc].sum;
}

// 对节点rt进行区间[l,r]的延迟更新操作,增加z
void down(int rt, int l, int r, ll z) {
    t[rt].sum += (r - l + 1) * z; // 更新区间和
    t[rt].lazy += z;              // 累加懒标记
}

// 将当前节点的懒标记下推到子节点
void pushdown(int rt, int l, int r) {
    if(t[rt].lazy == 0) return;   // 没有懒标记则直接返回
    int mid = (l + r) >> 1;
    down(lson, t[rt].lazy);       // 下推左子树
    down(rson, t[rt].lazy);       // 下推右子树
    t[rt].lazy = 0;               // 清空当前节点懒标记
}

// 构建线段树
void build(int rt, int l, int r) {
    if(l == r) {                  // 到达叶子节点
        t[rt].sum = a[l];         // 存储数组值
        t[rt].lazy = 0;           // 初始化懒标记
        return;
    }
    int mid = (l + r) / 2;
    build(lson);                  // 构建左子树
    build(rson);                  // 构建右子树
    pushup(rt);                   // 更新当前节点
}

// 区间[x,y]每个元素加z
void add(int rt, int l, int r, int x, int y, ll z) {
    if(r < x || y < l) return;    // 当前区间与目标区间无交集
    if(x <= l && r <= y) {        // 当前区间完全包含在目标区间内
        t[rt].sum += (r - l + 1) * z; // 更新区间和
        t[rt].lazy += z;              // 设置懒标记
        return;
    }
    int mid = (l + r) >> 1;
    pushdown(rt, l, r);           // 下推懒标记
    add(lson, x, y, z);           // 更新左子树
    add(rson, x, y, z);           // 更新右子树
    pushup(rt);                   // 更新当前节点
}

// 查询区间[x,y]的和
ll query(int rt, int l, int r, int x, int y) {
    if(r < x || y < l) return 0;  // 当前区间与查询区间无交集
    if(x <= l && r <= y) {        // 当前区间完全包含在查询区间内
        return t[rt].sum;         // 直接返回区间和
    }
    int mid = (l + r) / 2;
    pushdown(rt, l, r);           // 下推懒标记
    return query(lson, x, y) + query(rson, x, y); // 返回左右子树查询结果之和
}

int main() {
    scanf("%d%d", &n, &m);       // 读取数组长度和操作次数
    f(i,1,n) scanf("%lld", &a[i]); // 读取初始数组
    build(1,1,n);                // 构建线段树
    
    while(m--) {
        ll op, x, y, z;
        scanf("%lld", &op);        // 读取操作类型
        if(op == 1) {            // 区间加操作
            scanf("%lld%lld%lld", &x, &y, &z);
            add(1,1,n,x,y,z);
        }
        else {                   // 区间查询操作
            scanf("%lld%lld", &x, &y);
            printf("%lld\n", query(1,1,n,x,y));
        }
    }
    return 0;
}

 

解题思路

本题要求实现区间修改(区间加值)和区间查询(区间求和)两种操作。虽然题目名为"线段树模板",但这里采用了分块算法来实现,这是一种介于暴力与线段树之间的折中方案。

分块算法的核心思想是将数组分成若干块,对完整块进行整体操作,对不完整块进行暴力操作。这样可以在O(√n)的时间复杂度内完成每次操作,比纯暴力的O(n)更高效,又比线段树的实现更简单。

代码详细注释

#include<bits/stdc++.h>
#define ll long long  // 定义long long为ll,方便使用
using namespace std;
const int N = 1e5 + 10;  // 数组大小,比题目最大范围稍大

// 定义全局变量
ll a[N],      // 原始数组
   sum[N],    // 每个块的和
   lazy[N];   // 每个块的懒标记(记录未下放的加法操作)
int n, m,     // n是数组长度,m是操作次数
    block;    // 每个块的大小

// 区间修改函数:将区间[x,y]内每个数加上k
void change(int x, int y, ll k) {
    int bl = x / block,  // x所在块的编号
        br = y / block;  // y所在块的编号
    
    // 情况1:x和y在同一块内
    if (bl == br) {
        // 直接遍历区间内所有元素进行修改
        for (int i = x; i <= y; i++) {
            a[i] += k;      // 元素值增加k
            sum[bl] += k;   // 所在块的和增加k
        }
    } 
    // 情况2:x和y在不同块
    else {
        // 处理x所在的不完整块(前半部分)
        for (int i = x; i < (bl + 1) * block; i++) {
            a[i] += k;
            sum[bl] += k;
        }
        
        // 处理中间的完整块
        for (int i = bl + 1; i < br; i++) {
            lazy[i] += k;           // 懒标记增加k(表示整个块都加了k)
            sum[i] += k * block;    // 块和增加k*块大小
        }
        
        // 处理y所在的不完整块(后半部分)
        for (int i = br * block; i <= y; i++) {
            a[i] += k;
            sum[br] += k;
        }
    }
}

// 区间查询函数:查询区间[x,y]内所有数的和
ll query(int x, int y) {
    ll res = 0;  // 结果变量
    int bl = x / block,  // x所在块的编号
        br = y / block;  // y所在块的编号
    
    // 情况1:x和y在同一块内
    if (bl == br) {
        // 遍历区间内所有元素求和
        for (int i = x; i <= y; i++) {
            res += a[i] + lazy[bl];  // 元素值加上所在块的懒标记
        }
    } 
    // 情况2:x和y在不同块
    else {
        // 处理x所在的不完整块(前半部分)
        for (int i = x; i < (bl + 1) * block; i++) {
            res += a[i] + lazy[bl];
        }
        
        // 处理中间的完整块
        for (int i = bl + 1; i < br; i++) {
            res += sum[i];  // 直接加上整个块的和
        }
        
        // 处理y所在的不完整块(后半部分)
        for (int i = br * block; i <= y; i++) {
            res += a[i] + lazy[br];
        }
    }
    return res;
}

int main() {
    // 输入输出优化
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    // 读取数据
    cin >> n >> m;
    block = sqrt(n);  // 计算块大小(取平方根)
    
    // 初始化数组和块信息
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i / block] += a[i];  // 将元素值加入对应块的和
    }

    // 处理每个操作
    while (m--) {
        int op, x, y;
        ll k;
        cin >> op;
        
        // 操作1:区间加值
        if (op == 1) {
            cin >> x >> y >> k;
            change(x, y, k);
        } 
        // 操作2:区间查询
        else {
            cin >> x >> y;
            cout << query(x, y) << "\n";
        }
    }
    return 0;
}

 

posted @ 2025-05-27 15:11  CRt0729  阅读(52)  评论(0)    收藏  举报