打鼹鼠_二维树状数组(信息学奥赛一本通- P1540)(二维树状数组模版题)

【题目描述】

这是一道模板题。

给出一个 n×m 的零矩阵 A,你需要完成如下操作:

1xyk:表示元素 Ax,y自增 k;

2abcd:表示询问左上角为 (a,b),右下角为 (c,d) 的子矩阵内所有数的和。

【输入】

输入的第一行有两个正整数 n,m;

接下来若干行,每行一个操作,直到文件结束。

【输出】

对于每个 2 操作,输出一个整数,表示对于这个操作的回答。

【输入样例】

复制代码
2 2
1 1 1 3
1 2 2 4
2 1 1 2 2

【输出样例】

复制代码
7

【提示】

数据范围与提示:

对于 10% 的数据,n=1;

对于另 10% 的数据,m=1;

对于全部数据,1≤n,m≤2^12,1≤x,a,c≤n,1≤y,b,d≤m,∣k∣≤10^5 ,保证操作数目不超过 3×10^5 ,且询问的子矩阵存在。

一、 题目分析

【题目大意】 维护一个n×m的初始全为0的矩阵 A,要求高效支持两种操作:

  1. 单点修改:将矩阵中坐标为(x,y)的元素自增 k。

  2. 子矩阵求和:查询左上角为(a,b),右下角为 (c,d) 的子矩阵内所有元素的和。

【数据范围与核心矛盾】 1≤n,m≤4096,操作总数 ≤3×10^5。 如果每次修改是 O(1),查询用两层 for循环暴力遍历,查询的时间复杂度将高达O(N×M),在 30 万次操作下绝对会超时。我们必须将单次操作的复杂度降到对数级别,即O(logN×logM)。


二、 思考过程

  1. 一维到二维的推演 : 一维树状数组tree[x]维护的是一段长度为lowbit(x)的线状区间。因为二维平面的横纵坐标是绝对正交、互不干扰的,所以当我们升维到二维时,tree[x][y]维护的就是一个矩形面积 :宽度为lowbit(x),高度为lowbit(y)

  2. 修改操作(单点波及面) : 当 (x,y) 发生改变时,我们需要更新所有"管辖领地"包含 (x,y) 的大节点。在一维中是用for循环不断 i+=lowbit(i) 往后跳;在二维中,只需要将x轴的跳跃和y轴的跳跃进行双层嵌套组合即可。

  3. 查询操作(前缀矩阵和) : 同样利用双层嵌套,x-=lowbit(x) 配合 y-=lowbit(y),我们可以极其高效地拼凑出从左上角 (1,1) 到右下角 (x,y) 的整个前缀矩阵的面积和。


三、 解题思路

树状数组原生的 query(x,y) 只能求出"紧贴左上角"的前缀矩阵和。但题目要求的是任意一个悬空的子矩阵 (a,b) 到 (c,d) 的和。

这时候必须引入二维前缀和的核心------容斥原理: 要计算悬空子矩阵的面积,我们可以用大矩形挖去多余的部分:

  1. 先拿到大矩形:query(c,d)

  2. 挖掉它上方多余的矩形:-query(a-1,d)

  3. 挖掉它左边多余的矩形:-query(c,b-1)

  4. 因为左上角的矩形被挖了两次,必须补偿回来:+query(a-1,b-1)

最终公式:Ans=query(c,d)-query(c,b-1)-query(a-1,d)+query(a-1,b-1)


四、 算法设计

  • 数据结构 :二维数组c[5000][5000]。因为矩阵求和极易突破21亿的整型极限,该数组及查询函数的返回值必须使用long long

  • lowbit函数x&(-x),提取二进制最低位的1。

  • add函数 :双层for循环,控制变量均使用+=lowbit() 向上攀升更新。

  • query 函数 :双层for循环,控制变量均使用-=lowbit() 向下聚拢求和。

  • IO 优化 :使用 ios::sync_with_stdio(false); cin.tie(0); 应对 30 万次的庞大输入流。


五、 时空复杂度分析

  • 时间复杂度

    • 单次addquery的时间复杂度均为O(logN×logM)。

    • 总时间复杂度为O(Q×logN×logM),在N=4096时,logN≈12。单次操作最多只需循环144 次。30 万次操作的计算量在千万级别,1 秒内毫无压力。

  • 空间复杂度:需要开辟一个5000×5000的64位整数数组,空间复杂度O(N×M),约占用 200MB内存,符合常规竞赛的空间限制。


六、 易错点总结

  1. 容斥边界切勿漏减1 :在容斥原理公式中,必须是a-1b-1。如果写成ab,会把子矩阵边界上的那一行/那一列也给错误地挖掉。

  2. 全局变量与局部变量撞车 :我们以往的风格是将树状数组命名为 c。而在处理询问时,局部变量又定义了 int a,b,c,d;。虽然C++允许局部变量遮蔽全局变量,使得 query(c, d) 传递的是局部变量,但这在工程上是极度危险的做法。强烈建议将全局树状数组命名为cc,避免 Bug。

  3. 数据溢出 :二维求和极其庞大,一定要将ans和cc数组开成long long


七、 题解

本题是二维树状数组最纯粹的模板题。通过两层嵌套循环将一维的线段管辖扩展至二维的矩形管辖,再辅以二维前缀和的容斥原理,用极其轻量级的代码和极小的常数,完美解决了二维动态矩阵的维护问题。相比于二维线段树(树套树),二维树状数组是考场上兼顾速度与代码稳定性的最优解。


八、 完整代码

cpp 复制代码
#include <iostream>
using namespace std;
long long cc[5000][5000];//树状数组
int n,m;

//返回x二进制表示下最低位1所代表的整数
int lowbit(int x){
    return x&(-x);
}

//树状数组更新操作
void add(int x,int y,int k){
    //双层循环,x轴和y轴均向上攀升,更新所有包含(x,y)的大管辖矩阵
    for(int i=x;i<=n;i+=lowbit(i)){
        for(int j=y;j<=m;j+=lowbit(j)){
            cc[i][j]+=k;
        }
    }
}
//树状数组查询操作
//查询左上角为(1,1),右下角为(x,y)的前缀矩阵和
long long query(int x,int y){
    long long ret=0ll;//记录前缀和
    //双层循环,x轴和y轴均向下聚拢,无缝拼凑出前缀面积
    for(int i=x;i>0;i-=lowbit(i)){
        for(int j=y;j>0;j-=lowbit(j)){
            ret+=cc[i][j];
        }
    }
    return ret;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    int t;
    while(cin>>t){
        if(t==1){//自增操作
            int x,y,k;
            cin>>x>>y>>k;
            add(x,y,k);
        }
        else{//查询操作
            int a,b,c,d;
            cin>>a>>b>>c>>d;
            //套用二维前缀和容斥原理公式
            //务必注意是a-1和b-1,否则会多挖掉一行一列
            cout<<query(c,d)-query(c,b-1)-query(a-1,d)+query(a-1,b-1)<<"\n";
        }
    }
}
相关推荐
x_xbx2 小时前
LeetCode:198. 打家劫舍
算法·leetcode·职场和发展
_日拱一卒2 小时前
LeetCode:盛最多水的容器
数据结构·算法·leetcode
zyhomepage2 小时前
科技的成就(七十二)
开发语言·人工智能·科技·算法·内容运营
计算机安禾2 小时前
【数据结构与算法】第2篇:C语言核心机制回顾(一):指针、数组与结构体
c语言·开发语言·数据结构·c++·算法·链表·visual studio
dapeng28702 小时前
C++代码重构实战
开发语言·c++·算法
xu_wenming2 小时前
为什么要在项目中加入 ESP‑NN(神经网络)
mcu·物联网·算法·iot
juleskk2 小时前
3.22 复试训练
算法
还不秃顶的计科生2 小时前
力扣第84题:完全平方数
算法·leetcode·职场和发展
2301_776508722 小时前
分布式系统监控工具
开发语言·c++·算法