题解:洛谷 P10719 黑白格

【题目来源】

洛谷:P10719 [GESP202406 五级] 黑白格 - 洛谷

【题目描述】

小杨有一个 \(n\)\(m\) 列的网格图,其中每个格子要么是白色,要么是黑色。

小杨想知道至少包含 \(k\) 个黑色格子的最小子矩形包含了多少个格子。

【输入】

第一行包含三个正整数 \(n,m,k\),含义如题面所示。

之后 \(n\) 行,每行一个长度为 \(m\)\(01\) 串,代表网格图第 \(i\) 行格子的颜色,如果为 \(0\),则对应格子为白色,否则为黑色。

【输出】

输出一个整数,代表至少包含 \(k\) 个黑色格子的最小子矩形包含格子的数量,如果不存在则输出 \(0\)

【输入样例】

4 5 5
00000
01111
00011
00011

【输出样例】

6

【核心思想】

  1. 问题分析:给定 \(n \times m\)\(01\) 网格,求至少包含 \(k\) 个黑色格子(\(1\))的最小子矩形面积。这是一个二维前缀和 + 枚举 + 二分查找问题,核心在于枚举列区间后,在行方向上利用前缀和与二分查找快速定位满足条件的最小行区间。

  2. 算法选择

    • 行前缀和预处理\(sum[i][j]\) 表示第 \(i\) 行前 \(j\) 列的 \(1\) 的个数,快速计算任意列区间的和
    • 枚举列区间:固定左列 \(i\) 和右列 \(j\),将问题降维为行方向上的 \(1\) 维问题
    • 二分查找优化:在行方向上维护累计和数组,二分查找满足 \(\geq k\) 的最短行区间
  3. 关键步骤

    • 读入数据:读取 \(n, m, k\) 和网格
    • 行前缀和预处理\(i\)\(1\)\(n\)\(j\)\(1\)\(m\)):
      • \(sum[i][j] = sum[i][j-1] + w[i][j]\)
    • 枚举列区间\(i\)\(1\)\(m\)\(j\)\(i\)\(m\)):
      • 列宽 \(col = j - i + 1\)
      • 遍历每一行 \(l\),计算当前行在 \([i,j]\) 列区间内的 \(1\) 的个数:\(tmp = sum[l][j] - sum[l][i-1]\)
      • 维护累计和 \(now += tmp\),存入数组 \(num\)
      • \(now \geq k\):更新答案 \(ans = \min(ans, col \times l)\)
      • 二分查找:在 \(num\) 数组中二分查找最小的 \(L\),使得 \(now - num[L-1] \geq k\),更新 \(ans = \min(ans, col \times (l-L))\)
    • 输出结果\(ans\)(若 \(ans = 0\) 表示不存在)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m^2 \cdot n \cdot \log n)\),枚举列区间 \(O(m^2)\),每列区间处理 \(O(n \log n)\)
    • 空间复杂度:\(O(n \cdot m)\),前缀和数组
  5. 降维与二分查找的核心思想

    • 二维降一维:固定列区间 \([i,j]\) 后,每行在该区间内的 \(1\) 的个数可快速计算,问题转化为:在行方向上找一个连续子数组,使其和 \(\geq k\) 且长度最小
    • 前缀和加速区间查询:行前缀和将"列区间内 \(1\) 的个数"计算从 \(O(m)\) 降至 \(O(1)\)
    • 累计和 + 二分优化:维护从上到下的累计和数组,对于当前行 \(l\),需要找到最小的行号 \(L\),使得 \([L,l]\) 行区间内的 \(1\) 的总数 \(\geq k\)。利用累计和的单调性,通过二分查找在 \(O(\log n)\) 时间内定位
    • 面积计算公式:子矩形面积 = 列宽 \(\times\) 行高 = \((j-i+1) \times (l-L+1)\),注意代码中的边界处理
    • 适用于二维网格上的约束子矩形搜索、降维 + 二分优化类问题

【算法标签】

普及 #整数二分

【代码详解】

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

const int N = 110;
int w[N][N];    // 存储原始矩阵
int sum[N][N];  // 前缀和数组
int n, m;       // 矩阵的行数和列数

int main() {
    int k;
    cin >> n >> m >> k;  // 输入矩阵大小和阈值k

    // 读取矩阵数据并计算每行的前缀和
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= m; j++) {
            w[i][j] = s[j-1] - '0';  // 字符转数字
            sum[i][j] = sum[i][j-1] + w[i][j];  // 计算行前缀和
        }
    }

    int ans = 0;  // 存储最小面积

    // 枚举所有可能的列区间[i,j]
    for (int i = 1; i <= m; i++) {
        for (int j = i; j <= m; j++) {
            vector<int> num;  // 存储当前列区间的累计和
            int now = 0;      // 当前累计和

            // 遍历每一行
            for (int l = 1; l <= n; l++) {
                // 计算当前行在[i,j]列区间内的和
                int tmp = sum[l][j] - sum[l][i-1];
                now += tmp;
                num.push_back(now);

                // 如果累计和达到k,尝试更新最小面积
                if (now >= k) {
                    // 整个[1,l]行区间的面积
                    if (ans == 0)
                        ans = (j-i+1) * l;
                    else
                        ans = min(ans, (j-i+1) * l);

                    // 使用二分查找寻找更小的行区间
                    int L = 1, R = l;
                    while (L < R) {
                        int mid = (L + R + 1) >> 1;
                        if (now - num[mid-1] >= k)
                            L = mid;
                        else
                            R = mid - 1;
                    }

                    // 检查是否找到符合条件的子矩阵
                    if (now - num[L-1] >= k) {
                        if (ans == 0)
                            ans = (j-i+1) * (l-L);
                        else
                            ans = min(ans, (j-i+1) * (l-L));
                    }
                }
            }
        }
    }

    cout << ans << endl;  // 输出最小面积
    return 0;
}

【运行结果】

4 5 5
00000
01111
00011
00011
6
posted @ 2026-07-17 15:19  团爸讲算法  阅读(5)  评论(0)    收藏  举报