[CERC2016] 外观分析 Appearance Analysis

GitHub跳转原题关系图返回列表

先按全 # 边框切出所有窗口,再把每个窗口在允许旋转下做最小表示,用集合统计不同图案个数。

OJ: luogu

题目 ID: P3678

难度:普及/提高-

标签:模拟矩阵分类讨论

日期: 2026-06-21 14:15

题意

整张图片由很多大小完全相同的窗口组成,窗口之间和最外层边框都用一格 # 隔开。

每个窗口内部只会出现 .+,表示这个窗口的图案。

如果两个窗口的图案在旋转 0 / 90 / 180 / 270 度后能够完全重合,就认为它们是同一种图案;不允许翻转。

要求统计整张图片里一共有多少种本质不同的窗口图案。

思路

先看一个可以直接验证想法的朴素解:

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

int r, c;
vector<string> grid_data;
vector<int> border_rows, border_cols;

vector<string> rotate90(const vector<string> &mat) {
    int h = (int)mat.size();
    int w = (int)mat[0].size();
    vector<string> res(w, string(h, '.'));
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            res[j][h - 1 - i] = mat[i][j];
        }
    }
    return res;
}

// 直接判断两个窗口是否能通过旋转匹配。
bool same_pattern(vector<string> a, const vector<string> &b) {
    if ((int)a.size() == (int)b.size() && (int)a[0].size() == (int)b[0].size() && a == b) {
        return true;
    }
    for (int t = 1; t <= 3; t++) {
        a = rotate90(a);
        if ((int)a.size() == (int)b.size() && (int)a[0].size() == (int)b[0].size() && a == b) {
            return true;
        }
    }
    return false;
}

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

    cin >> r >> c;
    grid_data.resize(r);
    for (int i = 0; i < r; i++) {
        cin >> grid_data[i];
    }

    for (int i = 0; i < r; i++) {
        bool all_hash = true;
        for (int j = 0; j < c; j++) {
            if (grid_data[i][j] != '#') {
                all_hash = false;
                break;
            }
        }
        if (all_hash) {
            border_rows.push_back(i);
        }
    }
    for (int j = 0; j < c; j++) {
        bool all_hash = true;
        for (int i = 0; i < r; i++) {
            if (grid_data[i][j] != '#') {
                all_hash = false;
                break;
            }
        }
        if (all_hash) {
            border_cols.push_back(j);
        }
    }

    int win_h = border_rows[1] - border_rows[0] - 1;
    int win_w = border_cols[1] - border_cols[0] - 1;
    int row_cnt = (int)border_rows.size() - 1;
    int col_cnt = (int)border_cols.size() - 1;

    // brute.cpp:把所有窗口都拆出来,逐个和已有代表图案比较,直接判断旋转等价。
    vector<vector<string>> kinds;
    for (int i = 0; i < row_cnt; i++) {
        for (int j = 0; j < col_cnt; j++) {
            int sr = border_rows[i] + 1;
            int sc = border_cols[j] + 1;
            vector<string> mat(win_h);
            for (int x = 0; x < win_h; x++) {
                mat[x] = grid_data[sr + x].substr(sc, win_w);
            }

            bool found = false;
            for (int id = 0; id < (int)kinds.size(); id++) {
                if (same_pattern(mat, kinds[id])) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                kinds.push_back(mat);
            }
        }
    }

    cout << kinds.size() << '\n';

    return 0;
}

这题本身不难,关键是把窗口先正确切出来。

由于窗口之间的边框整行、整列都是 #,而窗口内部只会出现 .+,所以:

  • 一整行全是 # 的行,一定是横向边框
  • 一整列全是 # 的列,一定是纵向边框

找出所有这样的边框行和边框列后,相邻两条边框之间夹着的区域就是一个窗口。
所有窗口大小相同,因此第一个间隔就能确定窗口高和宽。

把每个窗口内容提取出来后,问题就变成:

如何判断两个窗口在旋转意义下是否等价?

做法是给每个窗口求一个“规范表示”:

  1. 记录原图案
  2. 再考虑旋转后的图案
  3. 在所有允许的旋转结果里,取字典序最小的编码字符串作为代表

这里有一个细节:

  • 如果窗口不是正方形,旋转 90 度和 270 度后长宽会交换,不能和原尺寸窗口匹配
  • 所以非正方形窗口只需要比较 0 度和 180
  • 正方形窗口才需要比较四种旋转

最后把每个窗口的规范表示丢进 set,集合大小就是答案。

代码

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

int r, c;
vector<string> grid_data;
vector<int> border_rows, border_cols;

// 把一个窗口图案编码成字符串,方便放进 set 去重。
string encode_pattern(const vector<string> &mat) {
    string res = "";
    int h = (int)mat.size();
    int w = (int)mat[0].size();
    res += to_string(h);
    res += "x";
    res += to_string(w);
    res += "|";
    for (int i = 0; i < h; i++) {
        res += mat[i];
        res += "/";
    }
    return res;
}

// 顺时针旋转 90 度。
vector<string> rotate90(const vector<string> &mat) {
    int h = (int)mat.size();
    int w = (int)mat[0].size();
    vector<string> res(w, string(h, '.'));
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            res[j][h - 1 - i] = mat[i][j];
        }
    }
    return res;
}

// 取一个窗口在允许旋转下的最小表示。
string canonical(const vector<string> &mat) {
    vector<string> cur = mat;
    string best = encode_pattern(cur);

    // 180 度旋转总是和原窗口同尺寸,因此一定要考虑。
    cur = rotate90(cur);
    if ((int)mat.size() == (int)mat[0].size()) {
        best = min(best, encode_pattern(cur));
    }

    cur = rotate90(cur);
    best = min(best, encode_pattern(cur));

    cur = rotate90(cur);
    if ((int)mat.size() == (int)mat[0].size()) {
        best = min(best, encode_pattern(cur));
    }

    return best;
}

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

    cin >> r >> c;
    grid_data.resize(r);
    for (int i = 0; i < r; i++) {
        cin >> grid_data[i];
    }

    for (int i = 0; i < r; i++) {
        bool all_hash = true;
        for (int j = 0; j < c; j++) {
            if (grid_data[i][j] != '#') {
                all_hash = false;
                break;
            }
        }
        if (all_hash) {
            border_rows.push_back(i);
        }
    }

    for (int j = 0; j < c; j++) {
        bool all_hash = true;
        for (int i = 0; i < r; i++) {
            if (grid_data[i][j] != '#') {
                all_hash = false;
                break;
            }
        }
        if (all_hash) {
            border_cols.push_back(j);
        }
    }

    int win_h = border_rows[1] - border_rows[0] - 1;
    int win_w = border_cols[1] - border_cols[0] - 1;
    int row_cnt = (int)border_rows.size() - 1;
    int col_cnt = (int)border_cols.size() - 1;

    set<string> kinds;

    for (int i = 0; i < row_cnt; i++) {
        for (int j = 0; j < col_cnt; j++) {
            int sr = border_rows[i] + 1;
            int sc = border_cols[j] + 1;
            vector<string> mat(win_h);
            for (int x = 0; x < win_h; x++) {
                mat[x] = grid_data[sr + x].substr(sc, win_w);
            }
            kinds.insert(canonical(mat));
        }
    }

    cout << kinds.size() << '\n';

    return 0;
}

复杂度

设整张图大小为 r × c

  • 扫描边框行和边框列:O(rc)O(rc)
  • 提取所有窗口并生成规范表示:总共仍然只会访问每个格子常数次,所以是 O(rc)O(rc)

总时间复杂度 O(rc)O(rc),空间复杂度 O(rc)O(rc)

总结

这题核心就是两步:

  1. 利用整行整列的 # 把窗口切出来
  2. 用旋转后的最小表示做去重

本质上是一道非常直接的网格模拟题。