题解:AcWing 796 子矩阵的和

【题目来源】

AcWing:796 子矩阵的和 - AcWing题库

【题目描述】

输入一个 \(n\)\(m\) 列的整数矩阵,再输入 \(q\) 个询问,每个询问包含四个整数 \(x_1,y_1,x_2,y_2\),表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

【输入】

第一行包含三个整数 \(n,m,q\)

接下来 \(n\) 行,每行包含 \(m\) 个整数,表示整数矩阵。

接下来 \(q\) 行,每行包含四个整数 \(x_1,y_1,x_2,y_2\),表示一组询问。

【输出】

\(q\) 行,每行输出一个询问的结果。

【输入样例】

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4

【输出样例】

17
27
21

【核心思想】

  1. 问题分析:给定 \(n \times m\) 的整数矩阵,以及 \(q\) 个子矩阵查询,每个查询由左上角 \((x_1, y_1)\) 和右下角 \((x_2, y_2)\) 定义,需要快速求出子矩阵内所有元素之和。若对每个查询暴力累加,时间复杂度为 \(O(q \cdot n \cdot m)\)。关键观察是:二维前缀和可以将任意子矩阵和转化为四个前缀和值的加减组合,实现 \(O(1)\) 查询。

  2. 算法选择

    • 二维前缀和:预处理 \(s[i][j] = \sum_{x=1}^{i} \sum_{y=1}^{j} a[x][y]\),则子矩阵和可通过容斥原理 \(O(1)\) 计算
    • 离线预处理 + 在线查询:先一次性计算二维前缀和,之后每个查询 \(O(1)\) 回答
  3. 关键步骤

    • 读取输入\(n\)(行数)、\(m\)(列数)、\(q\)(查询次数)、矩阵 \(a[1..n][1..m]\)
    • 预处理二维前缀和
      • \(s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]\)
      • 含义:上方前缀和 + 左方前缀和 \(-\) 重复计算的左上角前缀和 + 当前元素
    • 回答查询:对每个询问 \((x_1, y_1, x_2, y_2)\)
      • 输出 \(s[x_2][y_2] - s[x_2][y_1-1] - s[x_1-1][y_2] + s[x_1-1][y_1-1]\)
    • 原理:容斥原理,右下角前缀和 \(-\) 上方多余部分 \(-\) 左方多余部分 \(+\) 被重复减去的左上角部分
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m + q)\),预处理 \(O(n \cdot m)\),每次查询 \(O(1)\)
    • 空间复杂度:\(O(n \cdot m)\),存储原矩阵和二维前缀和数组
  5. 二维前缀和的核心思想

    • 容斥原理降维:将"子矩阵和"这一四维操作(需要两个坐标点)转化为二维前缀和数组的四个点值的容斥组合,实现 \(O(1)\) 查询
    • 递推构建\(s[i][j]\) 由上方、左方和左上角三个已计算的前缀和值递推得到,体现了动态规划的填表思想
    • 几何意义\(s[i][j]\) 表示以 \((1,1)\) 为左上角、\((i,j)\) 为右下角的矩形区域和,任意子矩形均可通过四个 such 矩形的加减得到
    • 边界处理:下标从 \(1\) 开始,\(s[0][j] = s[i][0] = 0\),使得第一行/列的计算无需特判,公式统一适用
    • 适用于静态矩阵的子矩阵和查询、图像区域统计、二维累积量计算等场景,是二维差分数组的逆运算

【解题思路】

【算法标签】

前缀和

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 1010;  // 定义二维数组最大尺寸

int n, m, q;        // n: 行数, m: 列数, q: 查询次数
int a[N][N];        // 存储原始二维数组
int s[N][N];        // 存储二维前缀和数组

int main()
{
    // 输入矩阵的行列数和查询次数
    scanf("%d%d%d", &n, &m, &q);

    // 输入矩阵元素
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &a[i][j]);
        }
    }

    // 初始化二维前缀和数组
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            // 计算前缀和公式:上方前缀和 + 左方前缀和 - 左上角前缀和 + 当前元素
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
        }
    }

    // 处理每个查询
    while (q--)
    {
        int x1, y1, x2, y2;  // 查询子矩阵的左上角和右下角坐标
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);

        // 计算子矩阵和公式:右下角前缀和 - 上方前缀和 - 左方前缀和 + 左上角前缀和
        printf("%d\n", s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
    }

    return 0;
}

【运行结果】

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
17
2 1 3 4
27
1 3 3 4
21
posted @ 2026-07-02 22:46  团爸讲算法  阅读(9)  评论(0)    收藏  举报