AcWing 4405. 统计子矩阵---前缀和 和 双指针的应用

给定一个 \(N \times M\) 的矩阵 \(A\),请你统计有多少个子矩阵 (最小 \(1 \times 1\),最大 \(N × M\)) 满足子矩阵中所有数的和不超过给定的整数 \(K\)?

输入格式

第一行包含三个整数 \(N, M\)\(K\)

之后 \(N\) 行每行包含 \(M\) 个整数,代表矩阵 \(A\)

输出格式

一个整数代表答案。

数据范围

对于 \(30\%\) 的数据,\(N, M ≤ 20\)
对于 \(70\%\) 的数据,\(N, M ≤ 100\)
对于 \(100\%\) 的数据,\(1 ≤ N, M ≤ 500; 0 ≤ A_{ij} ≤ 1000; 1 ≤ K ≤ 2.5 \times 10^8\)

输入样例:

3 4 10
1 2 3 4
5 6 7 8
9 10 11 12

输出样例:

19

样例解释

满足条件的子矩阵一共有 \(19\),包含:

  • 大小为 \(1 × 1\) 的有 \(10\) 个。
  • 大小为 \(1 × 2\) 的有 \(3\) 个。
  • 大小为 \(1 × 3\) 的有 \(2\) 个。
  • 大小为 \(1 × 4\) 的有 \(1\) 个。
  • 大小为 \(2 × 1\) 的有 \(3\) 个。

题解

本题我们首先应该想到的是暴力枚举所有子矩阵 算出其前缀和
利用算法基础课里所学的二维前缀和知识
https://www.acwing.com/solution/content/27301/
枚举每一个子矩阵 我们就要枚举其左上角的坐标 以及右下角的坐标
所以要枚举两对(x, y) 时间复杂度是\(O(n^4)\) 题目给的 \(1 <= N, M <= 500\)
那么我们计算的最大复杂度就会达到\(500^4\) 远超合理操作次数\(10^7\)
我们先写一个暴力做法 然后优化


暴力 \(O(n^4)\)
只能过6组数据

#include <bits/stdc++.h>

using namespace std;

const int N = 510;

int s[N][N];
int n, m, k;
long long cnt;

int main()
{
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1]; //输入数据并计算子矩阵前缀和
        }


    for (int x1 = 1; x1 <= n; x1 ++ )
        for (int y1 = 1; y1 <= m; y1 ++ )
            for (int x2 = x1; x2 <= n; x2 ++ )
                for (int y2 = y1; y2 <= m; y2 ++ )
                {
                    if (s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] <= k) //计算当前矩阵区间和 并与k比较 如果比k小就存下来
                    {
                        cnt ++ ;
                    }
                }

    printf("%lld", cnt);
}

利用双指针优化
我们来构思一下该怎么优化 暴力做法有四重循环 我们能否优化掉一层变成\(n^3\) 那么\(500^3\) 大约有\(10^8\) 是可以过的 我们利用双指针算法来优化
观察一下
b25ab3a014cf7ff817ec84b4715b227.jpg
由于l和r指针之间具有单调性 这样我们就可以优化掉一重循环了
注意我们是将列看成一个整体 利用l和r指针分别作为左右边界
AcWing-4405-统计子矩阵-蓝桥杯13届(2022)B组-AcWing.png

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

const int N = 510;
typedef long long LL;

int n, m, k;
int s[N][N];

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++) {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j]; //j不改变 求每一列上前i行的前缀和
        }

    LL res = 0;
    for (int i = 1; i <= n; i ++)
        for (int j = i; j <= n; j ++) //第j行确保在第i行底下 防止重复 注意!i和j都是枚举行 所以都小于n
        {
            for (int l = 1, r = 1, sum = 0; r <= m; r ++ )
            {
                sum += s[j][r] - s[i - 1][r]; //每次sum 加上 r指针向前移动一列所增加的 第i行到第j行在这列上的前缀和
               while (sum > k) //如果超出题目的要求 l指针往前移动 直到sum符合要求
               {
                   sum -= s[j][l] - s[i - 1][l]; //减去l指针向前移动减少的前缀和
                   l ++ ;
               }
               res += r - l + 1; //每次r指针向右移动一次都将范围内的子矩阵记录(l指针不一定移动) 是将当前以r指针为右边界的子矩阵都加上
            }
        }


    printf("%lld\n", res);          
    return 0;
}
posted @ 2024-04-16 16:26  MsEEi  阅读(38)  评论(0)    收藏  举报