把关于水平和竖直中线对称的四个格子分成一组,单点翻转时只更新这一组的贡献。
OJ: usaco
题目 ID: 1491
难度:普及-
标签:模拟计数
日期: 2026-07-11 12:15
题意
有一张
合法画布要求:它可以由某一个象限的图案,通过关于水平中线和竖直中线翻折得到。
换句话说,对于任意一个格子,它与上下、左右翻折后对应的格子必须最终全部相同。
现在给出一张被修改过的画布,并给出若干次单点翻转操作。需要在初始状态以及每次翻转后,输出让画布重新合法所需的最少修改次数。
思路
暴力想法
每个格子都属于一个关于水平中线、竖直中线对称的四元组。
对于代表元 (r,c),这一组 4 个位置是:
如果这一组里有 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;
}这个暴力每次更新都要扫描
增量维护
把格子 (r,c) 映射到它所在四元组的代表元:
维护 cnt[gr][gc],表示这一组中有多少个 #。
全局答案就是所有组的代价之和:
一次翻转 (r,c) 只会影响它所在的这一组。更新时按照下面的顺序做:
text
减去这一组旧代价
翻转 a[r][c],更新 cnt[gr][gc]
加上这一组新代价这样每次更新只做
代码
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;
}复杂度
初始化需要扫描整张图,时间复杂度
每次更新只处理一个四元组,时间复杂度
总时间复杂度
总结
这题的关键是不要把整张画布当成一个整体处理,而是拆成互不影响的对称四元组。
每组只有 4 个格子,最优修改次数就是 min(cnt,4-cnt)。
单点翻转只影响一组,所以维护每组 # 数量后,就可以快速更新全局答案。