Reflection

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

把关于水平和竖直中线对称的四个格子分成一组,单点翻转时只更新这一组的贡献。

OJ: usaco

题目 ID: 1491

难度:普及-

标签:模拟计数

日期: 2026-07-11 12:15

题意

有一张 N×NN \times N 的画布,NN 是偶数。

合法画布要求:它可以由某一个象限的图案,通过关于水平中线和竖直中线翻折得到。

换句话说,对于任意一个格子,它与上下、左右翻折后对应的格子必须最终全部相同。

现在给出一张被修改过的画布,并给出若干次单点翻转操作。需要在初始状态以及每次翻转后,输出让画布重新合法所需的最少修改次数。

思路

暴力想法

每个格子都属于一个关于水平中线、竖直中线对称的四元组。

对于代表元 (r,c),这一组 4 个位置是:

(r,c),  (n+1r,c),  (r,n+1c),  (n+1r,n+1c) (r,c),\;(n+1-r,c),\;(r,n+1-c),\;(n+1-r,n+1-c)

如果这一组里有 cnt#,那么:

  • 全部改成 .,需要改 cnt 个;
  • 全部改成 #,需要改 4-cnt 个。

所以这一组的最小代价是 min(cnt, 4-cnt)

每次更新后重新扫描所有四元组,就能得到一个朴素解:

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-11 12:15
 * update_at: 2026-07-11 12:20
 */
// brute.cpp:小数据暴力解,每次更新后重新扫描所有对称四元组。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 25;

int n, q;
char a[MAXN][MAXN];

int calc_one(int r, int c) {
    int cnt = 0;
    if (a[r][c] == '#') cnt++;
    if (a[n + 1 - r][c] == '#') cnt++;
    if (a[r][n + 1 - c] == '#') cnt++;
    if (a[n + 1 - r][n + 1 - c] == '#') cnt++;
    return min(cnt, 4 - cnt);
}

int calc_answer() {
    int ans = 0;
    int h = n / 2;
    for (int r = 1; r <= h; r++) {
        for (int c = 1; c <= h; c++) {
            ans += calc_one(r, c);
        }
    }
    return ans;
}

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

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= n; j++) {
            a[i][j] = s[j - 1];
        }
    }

    cout << calc_answer() << '\n';
    for (int i = 1; i <= q; i++) {
        int r, c;
        cin >> r >> c;
        a[r][c] = (a[r][c] == '#') ? '.' : '#';
        cout << calc_answer() << '\n';
    }

    return 0;
}

这个暴力每次更新都要扫描 O(N2)O(N^2) 个格子。数据范围里 UU 很大,所以需要利用“每次只翻转一个格子”这个条件。

增量维护

把格子 (r,c) 映射到它所在四元组的代表元:

gr=min(r,  n+1r),gc=min(c,  n+1c) gr = \min(r,\;n+1-r),\quad gc = \min(c,\;n+1-c)

维护 cnt[gr][gc],表示这一组中有多少个 #

全局答案就是所有组的代价之和:

ans=min(cnt[gr][gc],  4cnt[gr][gc]) ans = \sum \min(cnt[gr][gc],\;4-cnt[gr][gc])

一次翻转 (r,c) 只会影响它所在的这一组。更新时按照下面的顺序做:

text
减去这一组旧代价
翻转 a[r][c],更新 cnt[gr][gc]
加上这一组新代价

这样每次更新只做 O(1)O(1) 次计算。

代码

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-11 12:15
 * update_at: 2026-07-11 12:20
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2005;

int n, q;
char a[MAXN][MAXN];
int cnt[MAXN][MAXN]; // 每个对称四元组里的 '#' 数量
long long ans;

int group_row(int r) {
    // 映射到上下对称后的代表行。
    return min(r, n + 1 - r);
}

int group_col(int c) {
    // 映射到左右对称后的代表列。
    return min(c, n + 1 - c);
}

int cost(int x) {
    // 一个四元组里有 x 个 '#': 要么全部变 '#', 要么全部变 '.'。
    return min(x, 4 - x);
}

void add_cell(int r, int c) {
    if (a[r][c] == '#') {
        int gr = group_row(r);
        int gc = group_col(c);
        cnt[gr][gc]++;
    }
}

void read_input() {
    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= n; j++) {
            a[i][j] = s[j - 1];
            add_cell(i, j);
        }
    }

    int h = n / 2;
    for (int i = 1; i <= h; i++) {
        for (int j = 1; j <= h; j++) {
            ans += cost(cnt[i][j]);
        }
    }
}

void toggle_cell(int r, int c) {
    // 单点翻转只会改变它所在的一个对称四元组。
    int gr = group_row(r);
    int gc = group_col(c);

    ans -= cost(cnt[gr][gc]);
    if (a[r][c] == '#') {
        a[r][c] = '.';
        cnt[gr][gc]--;
    } else {
        a[r][c] = '#';
        cnt[gr][gc]++;
    }
    ans += cost(cnt[gr][gc]);
}

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

    read_input();

    cout << ans << '\n';
    for (int i = 1; i <= q; i++) {
        int r, c;
        cin >> r >> c;
        toggle_cell(r, c);
        cout << ans << '\n';
    }

    return 0;
}

复杂度

初始化需要扫描整张图,时间复杂度 O(N2)O(N^2)

每次更新只处理一个四元组,时间复杂度 O(1)O(1)

总时间复杂度 O(N2+U)O(N^2+U),空间复杂度 O(N2)O(N^2)

总结

这题的关键是不要把整张画布当成一个整体处理,而是拆成互不影响的对称四元组。

每组只有 4 个格子,最优修改次数就是 min(cnt,4-cnt)。 单点翻转只影响一组,所以维护每组 # 数量后,就可以快速更新全局答案。