水印检查

把每个 5x9 窗口转成阈值区间,用差分数组合并所有能呈现 CSP 水印的阈值。

OJ: shumeng

题目 ID: CSP202509B

难度:普及-

标签:枚举差分二维数组

日期: 2026-07-31 16:21

形式化题目

给定 n×nn \times n 灰度图,灰度值在 [0,L1][0, L-1]。对阈值 kk,灰度 k\ge k 的像素视为白色,否则黑色。求所有能在一个 5×95 \times 9 子矩阵中呈现固定 CSP 图案的阈值 kk,从小到大输出。

思路

先看最直接的做法:对每个阈值、每个窗口逐一比对。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 23:00
 */
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, limit;
    cin >> n >> limit;
    vector<vector<int> > image(n, vector<int>(n));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) cin >> image[i][j];
    }

    int pattern[5][9] = {
        {1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 0, 0, 1, 0, 0, 1, 0, 1},
        {1, 0, 0, 1, 1, 1, 1, 1, 0},
        {1, 0, 0, 0, 0, 1, 1, 0, 0},
        {1, 1, 1, 1, 1, 1, 1, 0, 0},
    };
    // 朴素做法:对每个阈值 k 逐窗口检查是否匹配水印图案,适合小数据对拍
    for (int k = 0; k < limit; k++) {
        bool found = false;
        for (int top = 0; top + 5 <= n && !found; top++) {
            for (int left = 0; left + 9 <= n && !found; left++) {
                bool valid = true;
                for (int i = 0; i < 5 && valid; i++) {
                    for (int j = 0; j < 9; j++) {
                        bool white = image[top + i][left + j] >= k;
                        if ((int)white != pattern[i][j]) {
                            valid = false;
                            break;
                        }
                    }
                }
                if (valid) found = true;
            }
        }
        if (found) cout << k << '\n';
    }
    return 0;
}

关键观察:阈值形成连续区间

固定一个 5×95 \times 9 窗口,图案中的白色位置必须满足 AkA \ge k,黑色位置必须满足 A<kA < k。因此该窗口能呈现水印的阈值是一个整数区间:

max(黑色位置灰度)+1kmin(白色位置灰度) \max(\text{黑色位置灰度}) + 1 \le k \le \min(\text{白色位置灰度})

差分合并

每个窗口只贡献一个区间。枚举所有窗口,用差分数组合并这些区间,最后扫描 0..L10..L-1 输出覆盖次数大于 0 的阈值即可。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 23:00
 */
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, limit;
    cin >> n >> limit;
    vector<vector<int> > image(n, vector<int>(n)); // 灰度图像
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) cin >> image[i][j];
    }

    // CSP 水印图案:1 表示白色(灰度>=k),0 表示黑色(灰度<k)
    int pattern[5][9] = {
        {1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 0, 0, 1, 0, 0, 1, 0, 1},
        {1, 0, 0, 1, 1, 1, 1, 1, 0},
        {1, 0, 0, 0, 0, 1, 1, 0, 0},
        {1, 1, 1, 1, 1, 1, 1, 0, 0},
    };

    // 差分数组:difference[k] 表示阈值 k 的覆盖次数变化量
    vector<int> difference(limit + 2, 0);
    for (int top = 0; top + 5 <= n; top++) {
        for (int left = 0; left + 9 <= n; left++) {
            // 该窗口能呈现水印的阈值区间:[黑色位置最大灰度+1, 白色位置最小灰度]
            int minimum_white = limit;
            int maximum_black = -1;
            for (int i = 0; i < 5; i++) {
                for (int j = 0; j < 9; j++) {
                    if (pattern[i][j] == 1) {
                        minimum_white = min(minimum_white, image[top + i][left + j]);
                    } else {
                        maximum_black = max(maximum_black, image[top + i][left + j]);
                    }
                }
            }
            int lower = maximum_black + 1;
            int upper = minimum_white;
            if (lower <= upper) {
                difference[lower]++;
                difference[upper + 1]--;
            }
        }
    }

    // 前缀和求每个阈值被覆盖的次数,大于 0 即可检测出水印
    int active = 0;
    for (int k = 0; k < limit; k++) {
        active += difference[k];
        if (active > 0) cout << k << '\n';
    }
    return 0;
}

复杂度

窗口数为 O(n2)O(n^2),每个窗口检查固定 45 个格子,总时间复杂度 O(n2)O(n^2),空间复杂度 O(n2+L)O(n^2 + L)

总结

阈值对固定窗口形成一个连续区间,先求区间再统一差分,比逐阈值检查每个窗口快得多。本题也可以先对所有窗口收集区间,再用扫描线求并,差分是最简单的实现。