【题】【树状数组】NKOJ 3702 打鼹鼠

本文介绍了一道名为NKOJ3702打鼹鼠的编程题,通过使用二维树状数组解决游戏中的鼹鼠计数问题。文章提供了完整的C++实现代码,展示了如何进行鼹鼠出现的更新以及查询指定区域内的鼹鼠总数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

NKOJ 3702 打鼹鼠
时间限制 : - MS 空间限制 : 65536 KB
评测说明 : 时限1000ms

问题描述
在这个“打鼹鼠”的游戏中,鼹鼠会不时地从洞中钻出来,不过不会从洞口钻进去。洞口都在一个大小为n(n<=1024)的正方形中。这个正方形在一个平面直角坐标系中,左下角为(0,0),右上角为(n-1,n-1)。洞口所在的位置都是整点,就是横纵坐标都为整数的点。
一个玩家不时地会想知道某一个范围的鼹鼠总数。请你快速回答。

输入格式
输入有若干行:
第一行,一个数n,表示鼹鼠的范围。
以后每一行开头都有一个数m,表示不同的操作:
m=1,那么后面跟着3个数x,y,k(0<=x,y < n),表示在点(x,y)处新出现了k只鼹鼠;
m=2,那么后面跟着4个数x1,y1,x2,y2(0<=x1<=x2< n,0<=y1<=y2< n),表示询问矩形(x1,y1)-(x2,y2)内的鼹鼠数量;
m=3,表示游戏结束,不能玩了。保证这个数会在输入的最后一行。

询问数不会超过10000,鼹鼠数不会超过maxlongint。

输出格式
对于每个m=2,输出一行数,这行数只有一个数,即所询问的区域内鼹鼠的个数。

样例输入
4
1 2 2 5
2 0 0 2 3
3

样例输出
5

来源 vijos1512

思路、
二维树状数组表示状态

#include<cstdio>
#include<iostream>
using namespace std;
#define lowbit(x) x&-x
const int need=1027;

int c[need][need];
int n;

void modify(int x,int y,int k)
{
    for(int x1=x;x1<=n;x1+=lowbit(x1))
     for(int y1=y;y1<=n;y1+=lowbit(y1)) 
      c[x1][y1]+=k;
}

int getsum(int x,int y)
{
    int ans=0;
    for(int x1=x;x1>0;x1-=lowbit(x1))
     for(int y1=y;y1>0;y1-=lowbit(y1))
      ans+=c[x1][y1];
    return ans; 
}

int main()
{
    scanf("%d",&n);
    int a,x,y,k,x1,y1;
    while(true)
    {
        scanf("%d",&a);
        if(a==3) return 0;
        if(a==1)
        {
            scanf("%d%d%d",&x,&y,&k);
            modify(x+1,y+1,k);
        } 
        else if(a==2)
        {
            scanf("%d%d%d%d",&x,&y,&x1,&y1);
            x++,y++,x1++,y1++;
            printf("%d\n",getsum(x1,y1)-getsum(x-1,y1)-getsum(x1,y-1)+getsum(x-1,y-1));
        }
    } 
} 
<think>我们正在解决打鼹鼠游戏中的问题,使用二维树状数组来高效处理区域查询和单点更新。 问题描述:有一个n*n的网格(n<=1024),初始时每个网格的鼹鼠数量为0。游戏中有两种操作: 1. 在某个位置(x,y)出现k只鼹鼠(或者理解成在(x,y)位置增加k只鼹鼠) 2. 查询一个矩形区域内的鼹鼠总数 二维树状数组(也称为二叉索引树,Fenwick Tree)可以高效地支持二维数组的单点更新和区域求和(在O(log n * log n)时间内)。 根据引用[3]中的代码示例,我们可以总结二维树状数组的基本操作: 1. lowbit(x): 取x的最低位1所对应的值,即x & -x。 2. 更新操作(Modify): 在位置(i, j)增加delta时,我们需要更新树状数组C。更新时,从(i,j)开始,沿着行和列方向不断加上lowbit,直到边界,更新所有相关的C[x][y]。 3. 求和操作(Sum): 求从(1,1)到(i,j)的矩形区域和。过从(i,j)开始,沿着行和列方向不断减去lowbit,直到0,累加所有相关的C[x][y]。 注意:目中网格坐标从(0,0)到(n-1,n-1),但树状数组常从1开始索引。因此,我们在处理输入坐标时,需要将其加1(即转换为1-based index)。 根据引用[2](vijos-1512)的描述,输入格式如下: 第一行:一个整数n,表示正方形区域的大小。 接下来若干行,每行有以下两种格式: 格式1: "1 x y k",表示在坐标(x,y)处增加了k只鼹鼠(注意:目中坐标是整点,且范围在[0, n-1])。 格式2: "2 x1 y1 x2 y2",表示查询矩形区域[x1,y1]到[x2,y2]内的鼹鼠总数。 输入以一行0结束。 使用二维树状数组解决该问题的步骤: 1. 初始化一个二维树状数组(大小为n+1 * n+1,初始全0)。 2. 读取操作: 如果操作类型为1:在(x,y)处增加k,则调用update(x+1, y+1, k)(因为树状数组下标从1开始)。 如果操作类型为2:查询矩形区域(x1,y1)到(x2,y2)的鼹鼠总数,则利用二维前缀和的思想: 区域和 = sum(x2+1, y2+1) - sum(x1, y2+1) - sum(x2+1, y1) + sum(x1, y1) (注意:由于树状数组的sum函数计算的是从(1,1)到(i,j)的和,而我们的坐标已经加1,所以原坐标(x,y)在树状数组中的位置是(x+1,y+1)) 因此,实际计算: ans = sum(x2+1, y2+1) - sum(x1, y2+1) - sum(x2+1, y1+1) + sum(x1, y1+1) ??? 这里需要调整 但是,注意:我们转换后,原坐标(x,y)对应树状数组中的(x+1,y+1)。那么矩形区域[x1, x2]和[y1, y2](原坐标)在树状数组中的区域为[x1+1, x2+1]和[y1+1, y2+1]。 根据二维前缀和,我们要计算的是: S = sum(x2+1, y2+1) - sum(x1, y2+1) - sum(x2+1, y1) + sum(x1, y1) 这里,sum(i,j)表示树状数组中(1,1)到(i,j)的和。 因此,我们定义: int query(int x, int y) // 返回(1,1)到(x,y)的和 那么矩形区域[x1, y1]到[x2, y2](原坐标)的和为: ans = query(x2+1, y2+1) - query(x1, y2+1) - query(x2+1, y1) + query(x1, y1) 但是注意:原坐标中,x1,y1是包含在矩形内的,而树状数组的查询是包含端点的。所以这个公式正确。 然而,在输入中,我们可能会遇到操作2的查询区域,其中x1,y1,x2,y2是原坐标(0-indexed),所以转换后: x1' = x1 + 1, y1' = y1 + 1, x2' = x2 + 1, y2' = y2 + 1 然后计算:ans = query(x2', y2') - query(x1', y2') - query(x2', y1') + query(x1', y1') 这个公式不对,正确的应该是: ans = query(x2', y2') - query(x1'-1, y2') - query(x2', y1'-1) + query(x1'-1, y1'-1) 实际上,二维前缀和的标准公式为: S(x1,y1,x2,y2) = S(x2,y2) - S(x1-1,y2) - S(x2,y1-1) + S(x1-1,y1-1) 因此,在树状数组中,我们计算的是从(1,1)到某个点的和,所以: ans = query(x2+1, y2+1) - query(x1, y2+1) - query(x2+1, y1) + query(x1, y1) 注意:这里x1,y1是原坐标,在树状数组中的位置是(x1+1,y1+1)?不对,我们转换后的坐标是(x+1,y+1)作为树状数组中的位置,那么: 原坐标(x,y)在树状数组中的位置是(x+1,y+1),所以原坐标的矩形区域[x1,x2]×[y1,y2]对应树状数组中的区域为[x1+1, y1+1]到[x2+1, y2+1]。 那么,我们要求这个区域的和,应该用: ans = query(x2+1, y2+1) - query(x1, y2+1) - query(x2+1, y1) + query(x1, y1) 但是,注意:query(i,j)求的是树状数组中[1,1]到[i,j]的和。那么: [x1+1, y1+1]到[x2+1, y2+1]的和 = query(x2+1, y2+1) - query(x1, y2+1) // 因为x1在树状数组中对应的是第x1行,但我们的矩形是从x1+1行开始的,所以减去[1,x1]行在[1,y2+1]的和 - query(x2+1, y1) // 减去[1,y1]列在[1,x2+1]行的和 + query(x1, y1) // 因为减了两次[1,x1]行[1,y1]列,所以要加回来 所以公式正确。 因此,代码实现: 1. 定义二维数组tree(大小为(n+1)*(n+1)),初始化为0。 2. 实现lowbit函数:int lowbit(int x) { return x & -x; } 3. 更新函数:void update(int x, int y, int delta) for (int i = x; i <= n; i += lowbit(i)) for (int j = y; j <= n; j += lowbit(j)) tree[i][j] += delta; 4. 查询函数:int query(int x, int y) // 查询(1,1)到(x,y)的和 int res = 0; for (int i = x; i > 0; i -= lowbit(i)) for (int j = y; j > 0; j -= lowbit(j)) res += tree[i][j]; return res; 5. 对于操作1:读取x, y, k -> 调用update(x+1, y+1, k) 6. 对于操作2:读取x1,y1,x2,y2 -> int total = query(x2+1, y2+1) - query(x1, y2+1) - query(x2+1, y1) + query(x1, y1); // 注意:这里的x1,y1,x2,y2是原坐标(0-indexed) 注意:目中n<=1024,所以二维树状数组的大小为1025*1025(因为下标从1到n,而n最大为1024,所以数组开1025*1025)。 但是,在输入中,操作类型为0时结束。 代码框架: #include <iostream> #include <cstring> using namespace std; const int maxn = 1030; // 1024+6 int n; int tree[maxn][maxn]; int lowbit(int x) { return x & -x; } void update(int x, int y, int delta) { for (int i = x; i <= n; i += lowbit(i)) for (int j = y; j <= n; j += lowbit(j)) tree[i][j] += delta; } int query(int x, int y) { int res = 0; for (int i = x; i; i -= lowbit(i)) // 注意:i>0 可以写成i,因为当i=0时,循环终止 for (int j = y; j; j -= lowbit(j)) res += tree[i][j]; return res; } int main() { // 注意:目输入可能有多个测试用例?但目描述没有说,所以按一个处理。 cin >> n; // 读入网格大小n // 初始化树状数组(全0) memset(tree, 0, sizeof(tree)); int op; while (cin >> op) { if (op == 0) break; if (op == 1) { int x, y, k; cin >> x >> y >> k; update(x+1, y+1, k); // 转换为1-based } else if (op == 2) { int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; // 计算区域和 int ans = query(x2+1, y2+1) - query(x1, y2+1) - query(x2+1, y1) + query(x1, y1); cout << ans << endl; } } return 0; } 但是,注意:目中说明,洞口的位置都是整点,且输入操作中的坐标都在[0, n-1]范围内。 然而,我们需要注意:在更新操作中,可能会出现负数k吗?目描述是“在(x,y)处增加k只鼹鼠”,所以k应该是正数(出现鼹鼠)或者负数(减少?目没有说,但根据引用[2]的目描述,应该是增加k只,所以k>=0)。但为了保险,我们按照输入处理。 但是,引用[2]的目描述中并没有提到减少鼹鼠,所以k应该是正数。但程序应该能处理负数(即减少)。 另外,注意:树状数组的更新和查询都是基于差分的,所以支持负数。 但是,上面的代码中,我们直接更新,没有判断边界,所以需要保证坐标在[1,n]范围内(因为n是网格大小,而坐标加1后最大为n,所以不会越界)。 然而,我们需要注意:当x1=0时,query(x1, y1)中的x1=0,在query函数中,0不会进入循环,所以返回0,正确。 因此,上述代码可以正确运行。 但是,在二维树状数组中,我们常使用1-based索引,所以转换后坐标范围在[1, n](因为原坐标加1后,最小为1,最大为n,因为原坐标最大为n-1,加1后为n)。 所以,我们开数组大小为n+1(行和列都是0..n,但是我们只使用1..n),所以更新和查询时,x和y在1到n之间。 但是,在更新操作中,x+1和y+1的范围是[1, n](因为原坐标x,y在[0, n-1]),所以x+1在[1, n]内,y+1在[1, n]内,不会越界。 因此,代码正确。 但是,注意:目中n<=1024,所以maxn=1030足够。 下面我们用C++代码实现(注意:目要求使用中文回答,但代码部分用C++,注释可以用中文)。 由于用户要求生成相关问题,我们在最后生成。 注意:用户要求回答中引用的段落末尾自然地添加引用标识,我们在引用参考的地方添加。 参考引用[2]和[3]的内容,我们实现了上述算法[^2][^3]。 但是,我们还需要注意:二维树状数组也可以使用差分的思想来支持区间更新和单点查询,但本是单点更新和区间查询,所以直接使用上述方法即可。 另外,引用[4]提到了差分数组,但本不需要区间更新,所以不需要二维差分。 因此,我们按照上述思路实现。 由于用户要求生成相关问题,我们在最后生成几个相关问题。 代码实现(C++):</think>### 二维树状数组在打鼹鼠游戏中的实现 在打鼹鼠游戏中,我们需要高效处理两种操作:(1) 在坐标 $(x,y)$ 增加 $k$ 只鼹鼠(单点更新)(2) 查询矩形区域 $[x1,y1]$ 到 $[x2,y2]$ 的鼹鼠总数(区域求和)。二维树状数组能在 $O(\log n \cdot \log n)$ 时间内完成这些操作,非常适合 $n \leq 1024$ 的场景[^2][^3]。 #### 核心算法实现(C++) ```cpp #include <iostream> #include <cstring> using namespace std; const int MAXN = 1030; // 1024+6 int tree[MAXN][MAXN]; // 二维树状数组 int n; // 网格大小 // 计算最低有效位 int lowbit(int x) { return x & -x; } // 单点更新:在(x,y)增加delta void update(int x, int y, int delta) { for (int i = x; i <= n; i += lowbit(i)) for (int j = y; j <= n; j += lowbit(j)) tree[i][j] += delta; } // 前缀和查询:返回(1,1)到(x,y)的矩形和 int query(int x, int y) { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) for (int j = y; j > 0; j -= lowbit(j)) res += tree[i][j]; return res; } int main() { cin >> n; // 读入网格大小 memset(tree, 0, sizeof(tree)); // 初始化 int op; while (cin >> op && op != 0) { if (op == 1) { // 单点更新 int x, y, k; cin >> x >> y >> k; update(x + 1, y + 1, k); // 坐标转换为1-based } else if (op == 2) { // 区域查询 int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; // 二维前缀和计算(转换为1-based坐标) int ans = query(x2 + 1, y2 + 1) - query(x1, y2 + 1) - query(x2 + 1, y1) + query(x1, y1); cout << ans << endl; } } return 0; } ``` #### 关键点解析 1. **坐标转换**: 游戏坐标 $(x,y)$(0-based)需转换为树状数组的1-based坐标 $(x+1,y+1)$,避免处理边界0[^3]。 2. **区域求和原理**: 矩形区域 $[x1,y1]$ 到 $[x2,y2]$ 的和过二维前缀和计算: $$ \text{sum} = Q(y_2+1,x_2+1) - Q(y_2+1,x_1) - Q(y_1,x_2+1) + Q(y_1,x_1) $$ 其中 $Q(a,b)$ 是查询函数 `query(a,b)`[^3]。 3. **时间复杂度**: - 单点更新:$O(\log n \cdot \log n)$ - 区域查询:$O(\log n \cdot \log n)$ 显著优于暴力方法 $O(n^2)$[^2]。 #### 示例操作流程 1. **初始化**:网格大小 $n=4$,所有位置初始0只鼹鼠。 2. **单点更新**:在 $(1,2)$ 增加3只鼹鼠 → `update(2,3,3)`(1-based坐标)。 3. **区域查询**:查询 $[0,1]$ 到 $[2,3]$ 的矩形区域: ```math \begin{align*} \text{ans} &= \text{query}(3,4) - \text{query}(0,4) - \text{query}(3,1) + \text{query}(0,1) \\ &= 3 - 0 - 0 + 0 = 3 \end{align*} ``` #### 性能优化建议 1. **差分技巧**:若需支持区间更新(如整个矩形区域增减),可结合差分数组[^4]。 2. **动态内存**:当 $n>1024$ 时改用动态分配的二维数组。 3. **输入缓冲**:对大规模数据使用 `scanf` 替代 `cin` 提高效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值