题解:洛谷 P9122 Stamp Grid

【题目来源】

洛谷:P9122 [USACO23FEB] Stamp Grid B - 洛谷

【题目描述】

盖章绘画是一幅黑白画,绘制在一个 \(N \times N\) 的画布上,其中某些格子被涂黑,而其他格子为空白。它可以用一个 \(N \times N\) 的字符数组表示(\(1 \leq N \leq 20\))。如果数组的第 \(i\) 行第 \(j\) 列的值为 *,说明该格子被涂黑;如果为 .,则说明该格子为空白。

Bessie 想要完成一幅盖章绘画,因此 Farmer John 借给了她一块 \(K \times K\)\(1 \leq K \leq N\))的盖章,以及一块空的 \(N \times N\) 画布。Bessie 可以将盖章顺时针旋转 \(90^\circ\),并在画布上的任意位置盖章,只要盖章完全在画布范围内即可。形式化地说,盖章时,Bessie 选择整数 \(i,j\),满足 \(i \in [1,N-K+1]\)\(j \in [1,N-K+1]\);对于每个 \((i',j')\),其中 \(1 \leq i',j' \leq K\),画布上的格子 \((i+i'-1,j+j'-1)\) 会被涂黑,如果盖章在 \((i',j')\) 处有墨迹。Bessie 可以在每次盖章之前旋转盖章。一旦画布上的某个格子被涂黑,就会保持涂黑状态。

Farmer John 想知道,Bessie 是否可以用他的盖章完成她想要的盖章绘画。对于每个 \(T\)\(1 \leq T \leq 100\))个测试用例,帮助 Farmer John 回答这个问题。

【输入】

第一行包含一个整数 \(T\),表示测试用例的数量。

每个测试用例以一个整数 \(N\) 开始,接下来是 \(N\) 行,每行包含由 *. 构成的字符串,表示 Bessie 想要的盖章绘画。接下来的行包含一个整数 \(K\),随后是 \(K\) 行,每行包含由 *. 构成的字符串,表示 Farmer John 的盖章。

相邻的测试用例之间用空行分隔。

【输出】

对于每个测试用例,输出一行 YESNO

【输入样例】

4

2
**
*.
1
*

3
.**
.**
***
2
.*
**

3
...
.*.
...
3
.*.
...
...

3
**.
.**
..*
2
.*
*.

【输出样例】

YES
YES
NO
YES

【核心思想】

  1. 问题分析:给定一个 \(N \times N\) 的目标图案(\(N \le 20\))和一个 \(K \times K\) 的印章图案(\(K \le N\)),印章可以旋转 \(0^\circ, 90^\circ, 180^\circ, 270^\circ\) 后,在空白画布上任意位置盖章(必须完全在画布内),每次盖章会将印章上 * 的位置涂黑,已涂黑的格子保持黑色不变。问能否通过若干次盖章(每个旋转角度可用任意次)完全复现目标图案。本质是一个有限状态空间内的模拟覆盖问题,由于 \(N\) 很小,直接枚举所有可能的盖章位置和旋转状态,进行“可覆盖性”标记即可。

  2. 算法选择

    • 模拟 + 贪心覆盖:由于盖章只会增加黑色格子(不会擦除),且目标图案是确定的,我们只需在空白画布上,对所有可能的位置($ (N-K+1)^2 $ 种)和 4 种旋转状态,尝试盖章。但必须注意:不能盲目盖章——如果印章上某个 * 对应的目标画布位置是 .,则在该位置用该旋转状态盖章会破坏目标(染黑不该黑的位置),因此该操作非法。合法的盖章操作必须满足“印章上所有 * 位置在目标图案中也是 *”。
    • 多次覆盖合法化:对一个位置,即使某次旋转状态非法(会盖到 .),换一个旋转状态可能就合法了。将所有合法的盖章操作都执行一遍(即把所有能被合法盖到的目标 * 都涂黑),最后检查目标图案中所有 * 是否都已被覆盖。由于盖章只会增加黑格,且所有合法盖章操作之间不冲突(因为都只覆盖目标中的 *),所以“执行所有合法操作”是安全的,且能得到最大覆盖范围。
  3. 关键步骤(基于学而思课堂笔记的清晰实现):

    • 数据读入:读入测试用例数 \(T\)。对每个用例,读入 \(N\) 和目标图案 \(a\)(1-based 索引),初始化空白画布 \(d\) 全为 .。读入 \(K\) 和印章图案 \(c\)
    • 四方向旋转枚举:循环 \(p = 1 \dots 4\) 次,每次先将印章顺时针旋转 \(90^\circ\)(使用过渡数组 \(t\)),然后枚举所有可能的盖章左上角位置 \((x, y)\),其中 \(1 \le x \le N-K+1\)\(1 \le y \le N-K+1\)
    • 合法性检查与盖章(函数 paint(x, y)):
      • 遍历印章 \(c\) 的所有位置 \((i, j)\)\(1 \le i,j \le K\)),如果 \(c[i][j] == '*'\) 且目标图案 \(a[x+i-1][y+j-1] != '*'\),则此次盖章非法,直接返回(因为会覆盖空白区域)。
      • 若所有 * 位置在目标中均为 *,则执行盖章:遍历印章,对 \(c[i][j] == '*'\) 的格子,将画布 \(d[x+i-1][y+j-1]\) 设为 *
    • 最终检查(函数 check()):遍历目标图案 \(a\) 的所有位置,如果存在 \(a[i][j] == '*'\)\(d[i][j] != '*'\),说明该黑格无法被任何合法盖章覆盖,输出 NO;否则输出 YES
  4. 时间/空间复杂度

    • 时间复杂度:\(O(T \cdot 4 \cdot (N-K+1)^2 \cdot K^2)\)。最坏情况下 \(N=20, K=20\),则 \((N-K+1)^2=1\)\(K^2=400\),常数极小;若 \(K=1\),则 \((N-K+1)^2=400\)\(K^2=1\)。总体每次检查最多约 \(4 \times 400 \times 400 = 640,000\) 次操作,\(T \le 100\),完全可行。
    • 空间复杂度:\(O(N^2 + K^2)\),用于存储图案和印章。
  5. 模拟覆盖的核心思想

    • “合法操作全集”策略:因为盖章操作只会增加黑色,且所有操作独立(不会相互抵消),所以执行所有合法的盖章操作(即不覆盖目标中 . 的操作)能得到最优覆盖结果。如果这样还不能覆盖所有目标 *,则其他任何操作序列也不可能做到。
    • 旋转枚举:每次盖章前可以旋转,等价于我们有 4 个不同的印章模板,每个模板可以在任意位置使用任意次。只需把这 4 个模板在每个位置都尝试一次即可。
    • 避免过度染色:合法性检查是核心——印章上的 * 必须落在目标图案的 * 上,否则该次盖章会永久留下多余的黑点,且无法擦除,导致失败。
    • 适用场景:适用于小规模网格(\(N \le 20\))的“拼图覆盖”类问题,核心是判断一组可旋转的模板能否通过多次合法放置完全覆盖目标图案中的特定标记位置。

【解题思路】

【算法标签】

普及- #模拟

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int t, n, k;
char a1[25][25], a2[25][25], b1[25][25], b2[25][25], b3[25][25], b4[25][25];
void paint(char a[][25], char b[][25], char a2[][25])  // 定义对比并画画的自定义函数
{
    for (int i=0; i<n-k+1; i++) {  // a1矩阵的顶点坐标i和j,从0 - n-k+1
        for (int j=0; j<n-k+1; j++) {
            int mark = 1;  // 定义标记位mark,初始为1
            for (int x=i; x<i+k; x++) {  // 遍历画布中邮票大小的区域
                for (int y=j; y<j+k; y++) {
                    if (b[x-i][y-j]=='*' && a[x][y]!='*') {  // 该区域与邮票对比(使用x-i和y-j,保证邮票的坐标永远是0,0 0,1 1,0 1,1),对于邮票中的'*',如果目的邮票画对应的位置也为'*'
                        mark = 0;  // 修改标记位为0
                        break;  // 退出循环
                    }
                }
                if (mark==0) break;  // 如果mark为0,退出循环
            }
            if (mark == 1) {  // 如果mark为1
                for (int x = i; x<i+k; x++) {  // 遍历画布中邮票大小的区域
                    for (int y=j; y<j+k; y++) {
                        if (b[x-i][y-j]=='*' && a2[x][y]=='.') {  // 对于邮票涂墨且画布中空白的地方
                            a2[x][y] = b[x-i][y-j];  //在画布的对应位置涂成黑色
                        }
                    }
                }
            }
        }
    }
}
int main()
{
    cin >> t;  // 输入t组数据
    while (t--) {  // 遍历t组数据
        cin >> n;  // 输入n
        for (int i=0; i<n; i++) {  // 输入n*n的邮票画
            for (int j=0; j<n; j++) {
                cin >> a1[i][j];
            }
        }
        cin >> k;  // 输入k
        for (int i=0; i<k; i++) {  // 输入k*k的邮票
            for (int j=0; j<k; j++) {
                cin >> b1[i][j];
            }
        }
        for (int i=0; i<k; i++) {  // 旋转90度
            for (int j=0; j<k; j++) {
                b2[i][j] = b1[k-1-j][i];
            }
        }
        for (int i=0; i<k; i++) {  // 再旋转90度(相当于原有邮票旋转180度)
            for (int j=0; j<k; j++) {
                b3[i][j] = b2[k-1-j][i];
            }
        }
        for (int i=0; i<k; i++) {  // 再旋转90度(相当于原有邮票旋转270度)
            for (int j=0; j<k; j++) {
                b4[i][j] = b3[k-1-j][i];
            }
        }
        memset(a2, '.', sizeof(a2));  // 每次初始化n*n的画布

        paint(a1, b1, a2);  // 用b1去比对a1,并画画
        paint(a1, b2, a2);  // 用b2去比对a1,并画画
        paint(a1, b3, a2);  // 用b3去比对a1,并画画
        paint(a1, b4, a2);  // 用b4去比对a1,并画画

        int mark = 0;  // 定义mark标记位
        for (int i=0; i<n; i++) {  // 检查a1与a2是否完全匹配
            for (int j=0; j<n; j++) {
                if (a1[i][j]==a2[i][j]) mark = 1;  // 匹配时mark标记为1
                else {
                    mark = 0;  // 如果遇到有个不匹配的,mark修改为0
                    break;  // 并退出循环
                }
            }
            if (mark==0) break;  // 如果mark为0,再退出外部循环
        }
        if (mark==1) cout << "YES" << endl;  // 最后mark为1就输出YES
        else cout << "NO" << endl;  // 否则输出NO
    }
    return 0;
}
//  学而思课堂笔记
#include <bits/stdc++.h>
using namespace std;
int T, n, k;
char a[25][25], c[25][25], t[25][25], d[25][25];  // a表示目标画作,c表示印章,t表示印章旋转90度的过渡数组,d表示空白画布
void rotate()  // 将印章顺时针旋转90°,
{
    // 第一步:将印章数组c顺时针旋转90°存入过渡数组t
    for (int i=1; i<=k; i++) {
        for (int j=1; j<=k; j++) {
            t[j][k+1-i] = c[i][j];
        }
    }
    // 第二步:将过渡数组t复制回原印章数组c
    for (int i=1; i<=k; i++) {
        for (int j=1; j<=k; j++) {
            c[i][j] = t[i][j];
        }
    }
}
void paint(int x, int y)  // 在空白画布(数组d)上盖章染色(印章的左上角与画布的(x,y)重叠)
{
    // 第一步:检查此处是否可以盖章染色
    for (int i=x; i<=x+k-1; i++) {
        for (int j=y; j<=y+k-1; j++) {
            if (c[i-x+1][j-y+1]=='*' && a[i][j]!='*') {
                return;
            }
        }
    }
    // 第二步:第一步没有return表示此处可以盖章染色,就染色即可
    for (int i=x; i<=x+k-1; i++) {
        for (int j=y; j<=y+k-1; j++) {
            if (c[i-x+1][j-y+1]=='*') {
                d[i][j] = '*';
            }
        }
    }
}
void check()
{
    for (int i=1; i<=n; i++) {
        for (int j=1; j<=n; j++) {
            if (a[i][j]=='*' && d[i][j]!='*') {
                cout << "NO" << endl;
                return;
            }
        }
    }
    cout << "YES" << endl;
}
int main()
{
    cin >> T;
    while (T--) {
        cin >> n;
        for (int i=1; i<=n; i++) {
            for (int j=1; j<=n; j++) {
                cin >> a[i][j];
                d[i][j] = '.';
            }
        }
        cin >> k;
        for (int i=1; i<=k; i++) {
            for (int j=1; j<=k; j++) {
                cin >> c[i][j];
            }
        }
        for (int p=1; p<=4; p++) {
            rotate();
            for (int x=1; x<=n-k+1; x++) {
                for (int y=1; y<=n-k+1; y++) {
                    paint(x, y);
                }
            }
        }
        check();
    }
    return 0;
}

【运行结果】

4

2
**
*.
1
*
YES

3
.**
.**
***
2
.*
**
YES

3
...
.*.
...
3
.*.
...
...
NO

3
**.
.**
..*
2
.*
*.
YES
posted @ 2026-08-29 09:41  团爸讲算法  阅读(6)  评论(0)    收藏  举报