按 (i+j) mod 4 给格子分成四类,每条 1×4 恰好占四类各一个,于是 0 的下界是最小类大小;用「第 i 行只在 j≡p[i mod 4] 放 0」的 4 行周期排列即可取到这个下界。
OJ: luogu
题目 ID: P9727
难度:提高+/省选-
标签:构造数学抽屉原理分类讨论
创建: 2026-10-02 15:23
更新: 2026-10-03 17:20
形式化题目
给定
- 横向或纵向都不存在连续四个同号的格子;
- 所有填
的格子四连通(按共边相邻)。
要求
样例 第一组
11101110
10111011
11011011暴力解法
思路
只要
把「没有四个连续的
于是可以按
- 取一个下界
:横向、纵向各自能塞下的互不重叠的 连段数,就是 个数的一个下界。 - 对
依次尝试:用最朴素的方式枚举大小不超过 的命中集——找到第一条还没被命中的 连段,枚举用它的 个格子之一下去命中(这条连段必须有 ,可以不失一般性地假设 就落在它里面),递归时剩余额度减一。 - 一旦所有
连段都被命中,再检查剩下的 是否四连通;是就说明答案为 ,立即输出。
第一个能搜到的
代码
/**
* 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-10-02 15:23
* update_at: 2026-10-03 16:55
*/
// P9727 暴力对拍解:求「最少 0 的个数」。
//
// 关键观察(只用来做等价变换,不做任何关于答案的猜测):
// 「没有 4 个连续的 1」 等价于 「所有长度为 4 的横条和竖条都至少含一个 0」。
// 也就是说,0 的集合必须是所有 4 连段的「命中集」。于是暴力可以这样枚举:
//
// 设下界 low = max(n*(m/4), m*(n/4))(横向 / 纵向各自能塞下的互不相交 4 连段数),
// 对每个 z = low, low+1, ... 依次尝试:
// 用最朴素的方式枚举大小恰好为 z 的命中集(每次挑「第一条还没被命中的
// 4 连段」,枚举用它的 4 个格子之一下去命中),
// 对每个枚举到的 0 集,检查剩下的 1 是否四连通;
// 只要找到一个就说明答案是 z,立即输出。
//
// 这样枚举的规模远小于 2^(nm),而且完全正确、不做任何假设:
// 第一层循环的 z 就是答案,dfs 没有找到任何可行解才会增大 z。
// 只用于对拍的小数据(生成器保证 n*m <= 36,此时单边最大是 2 x 18,
// 所以 MAXN 要开到 18 以上,否则 2 x 17 这类瘦长条会越界)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20;
int n, m;
int seg[4 * MAXN * MAXN][4][2]; // 每条 4 连段的 4 个格子坐标
int segCnt;
bool zero[MAXN][MAXN]; // 当前被选中当 0 的格子
int sel[MAXN][MAXN]; // sel[i][j] = 该格子的 0 是在第几层被选的
// 当前所有 4 连段是否都已经被命中
int find_unhit() {
for (int s = 0; s < segCnt; s++) {
bool hit = false;
for (int k = 0; k < 4; k++)
if (zero[seg[s][k][0]][seg[s][k][1]]) { hit = true; break; }
if (!hit) return s;
}
return -1;
}
// 检查 1 是否四连通(没有 1 视为不合法)
bool connected() {
static int qx[MAXN * MAXN], qy[MAXN * MAXN];
static bool vis[MAXN][MAXN];
int si = -1, sj = -1, ones = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (!zero[i][j]) {
ones++;
if (si < 0) { si = i; sj = j; }
}
if (ones == 0) return false;
memset(vis, 0, sizeof(vis));
int head = 0, tail = 0, cnt = 0;
qx[tail] = si; qy[tail] = sj; tail++; vis[si][sj] = true;
int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
while (head < tail) {
int x = qx[head], y = qy[head]; head++; cnt++;
for (int d = 0; d < 4; d++) {
int a = x + dx[d], b = y + dy[d];
if (a < 0 || b < 0 || a >= n || b >= m) continue;
if (zero[a][b] || vis[a][b]) continue;
vis[a][b] = true;
qx[tail] = a; qy[tail] = b; tail++;
}
}
return cnt == ones;
}
// 已经放了 used 个 0,还允许再放 left 个;找大小 <= z 的可行命中集
bool dfs(int left) {
int s = find_unhit();
if (s < 0) return connected(); // 已命中所有 4 连段,只需再检查连通性
if (left == 0) return false;
for (int k = 0; k < 4; k++) {
int x = seg[s][k][0], y = seg[s][k][1];
if (zero[x][y]) continue;
zero[x][y] = true;
if (dfs(left - 1)) return true;
zero[x][y] = false;
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n >> m;
segCnt = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j + 3 < m; j++) {
for (int k = 0; k < 4; k++) { seg[segCnt][k][0] = i; seg[segCnt][k][1] = j + k; }
segCnt++;
}
for (int j = 0; j < m; j++)
for (int i = 0; i + 3 < n; i++) {
for (int k = 0; k < 4; k++) { seg[segCnt][k][0] = i + k; seg[segCnt][k][1] = j; }
segCnt++;
}
memset(zero, 0, sizeof(zero));
int low = max(n * (m / 4), m * (n / 4));
int ans = -1;
for (int z = low; z <= n * m; z++) {
memset(zero, 0, sizeof(zero));
if (dfs(z)) { ans = z; break; }
}
cout << n * m - ans << '\n';
}
return 0;
}复杂度
单次搜索的分支数上界是
瓶颈
瓶颈在于「逐个格子决定放不放
回看
| 行 |
||
|---|---|---|
第
这正是把指数搜索压成
正解
思路
下界: 打包与四类标号
给每个格子
这条性质同时给出两个结论。
结论一(打包下界)。 取一组两两不相交的
结论二(
当
四类大小的公式
若
直接按
也就是说,只要
其中
构造: 行周期的排列
现在来造一个恰好用
即第
关键点是:不同的
0 2 1 3 |
0 1 3 2 |
0 2 1 3 |
0 1 3 2 |
|
0 1 3 2 |
1 2 0 3 |
2 0 1 3 |
3 0 2 1 |
|
0 2 1 3 |
1 2 0 3 |
2 3 1 0 |
0 3 1 2 |
|
0 1 3 2 |
1 3 2 0 |
0 2 3 1 |
0 1 3 2 |
例如
为什么
题面允许多解(
If there are multiple solution, output any),所以具体取哪一个并不重要,只要 的个数等于 且矩阵合法即可。这也是本题对拍必须用 special judge 的原因(见文末)。
瘦长情形:
: 总共不超过 格, ,全填 一定合法,答案为 。 : 的盘面。若某一列上下都是 ,这一列就成了把左右隔开的"墙", 无法连通;若连续两列都是 ,又会与紧邻的列拼出纵向……总之需要仔细安排。答案是 一种达到它的构造是让两行的列错开:第 行取 、第 行取 ,于是每个 周期块里恰有 个 (比重叠放置更省),且每一列都还有 可供上下连通。 : 时答案是 其中来自每个 周期块恰好 个 (每行一个),余下的 列里还剩 列时需要额外补一个 。取周期 即可达到; 是官方题解点名需要特殊构造的那一档。
算法流程
对每个测试点:
- 若
且 :全填 ,最少 的个数为 。 - 否则设
、 : :按 用周期 填,若 则转置输出; :按 用周期 填,若 则转置输出; :查表取 ,第 行的 放在 。
- 按公式算出最少
的个数 ,第一行输出 ,随后输出整个矩阵。
代码
/**
* 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-10-02 15:23
* update_at: 2026-10-03 16:55
*/
// P9727 [EC Final 2022] Aqre
//
// 目标:填 0/1,不能有 4 个连续的同号(横竖都不行),1 必须四连通,且 1 尽量多
// (等价于 0 尽量少)。答案要输出最多的 1 的个数和任意一个合法矩阵。
//
// 【下界】把盘面按 (i+j) mod 4 分成 4 类。任意一个 1x4 的横条或竖条都恰好包含
// 4 个类各一个,所以它内部至少有 1 个 0。于是只要能放 t 个互不相交的 1x4,
// 就有 至少 t 个 0。再注意到同一类里任意两个格子之间的曼哈顿距离都是 4 的倍数,
// 不可能落在同一个 1x4 条里(1x4 条要求 4 个格子连续且刚好 4 长),
// 所以「某一类全取成 0」本身就是一个可行解,代价是该类的大小。
//
// 【结论】最少 0 的个数 z 为:
// n,m <= 3 : z = 0
// min = 2, max = b : z = b / 2
// min = 3, max = b : z = 3 * (b / 4) + (b % 4 >= 2 ? 1 : 0)
// n,m >= 4 : z = n*m/4 - (n%4==2 && m%4==2 ? 1 : 0)
// 其中 n,m >= 4 的情形,n*m/4 恰好等于 (i+j) mod 4 四类中最小类的大小(见下),
// n%4==2 且 m%4==2 时可以比「某一类全 0」再多省一个(多出来的那个 0 来自
// 每行/每列的 4 周期结构正好让两个相邻行错开,从而四类各少一个)。
//
// 【构造】对 n,m >= 4,用「按 4 行循环」的周期结构:
// 第 i 行的 0 只出现在满足 j % 4 == p[i % 4] 的那些列,p 是 0..3 的排列。
// 这样一个 4x4 的周期块里每行每列各有 1 个 0,每类 (i+j)%4 也各有 1 个 0,
// 于是 0 的总数正好等于「若 n,m 都是 4 的倍数」时为 n*m/4 ;
// 当 n,m 不是 4 的倍数时,多出来的那些行/列会按 p 的值把某个类多算几次,
// 只要选对排列 p,总数就正好等于上面的公式,并且连通性也成立。
// 不同 (n%4, m%4) 需要不同的 p,共 16 种情况,直接查表(表由程序离线穷举
// 所有 4! = 24 个排列、逐个验证公式与连通性得到,见 verify 脚本)。
//
// 对 min(n,m) <= 3 的小情形:先把短边方向当作「行数 a = min(n,m)」按
// a x b 构造(a = 2 用周期 [1,3],a = 3 用周期 [1,3,3]),
// 若原始 n > m 再转置回来。n,m <= 3 时全填 1 即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
int g[MAXN][MAXN]; // 0/1 矩阵
// TAB[n%4][m%4]:n,m >= 4 时第 i 行的 0 所在列满足 j%4 == TAB[n%4][m%4][i%4]
static const int TAB[4][4][4] = {
{{0, 2, 1, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 1, 3, 2}},
{{0, 1, 3, 2}, {1, 2, 0, 3}, {2, 0, 1, 3}, {3, 0, 2, 1}},
{{0, 2, 1, 3}, {1, 2, 0, 3}, {2, 3, 1, 0}, {0, 3, 1, 2}},
{{0, 1, 3, 2}, {1, 3, 2, 0}, {0, 2, 3, 1}, {0, 1, 3, 2}},
};
// 最少 0 的个数
long long min_zeros(int nn, int mm) {
if (nn <= 3 && mm <= 3) return 0;
int a = nn < mm ? nn : mm;
int b = nn < mm ? mm : nn;
if (a == 2) return b / 2;
if (a == 3) return 3LL * (b / 4) + (b % 4 >= 2 ? 1 : 0);
long long res = (long long)nn * mm / 4;
if (nn % 4 == 2 && mm % 4 == 2) res -= 1;
return res;
}
// 把 a x b(a <= 3)按行周期 q 填进 g,need_trans 表示最终要转置回来
void build_small(int a, int b, const int *q, int qlen, bool need_trans) {
for (int i = 0; i < a; i++) {
for (int j = 0; j < b; j++) {
int v = (j % 4 == q[i % qlen]) ? 0 : 1;
if (need_trans) g[j][i] = v;
else g[i][j] = v;
}
}
}
void solve() {
if (n <= 3 && m <= 3) {
// 格子太少,全填 1 一定合法(不可能出现 4 个连续)
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) g[i][j] = 1;
return;
}
int a = n < m ? n : m;
int b = n < m ? m : n;
if (a <= 3) {
bool need_trans = (n != a);
if (a == 2) {
int q[2] = {1, 3};
build_small(a, b, q, 2, need_trans);
} else {
int q[3] = {1, 3, 3};
build_small(a, b, q, 3, need_trans);
}
return;
}
const int *p = TAB[n % 4][m % 4];
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) g[i][j] = (j % 4 == p[i % 4]) ? 0 : 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n >> m;
solve();
long long zeros = min_zeros(n, m);
cout << (long long)n * m - zeros << '\n';
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) cout << g[i][j];
cout << '\n';
}
}
return 0;
}复杂度
- 每个测试点:构造并输出
个格子;查表和算 是 。 - 总时间
,总空间 (实现里开了 的全局 int数组,约MiB)。
总结
- 四类标号是全部线索。 按
分类后,每条 恰好含四类各一个,于是互不相交的 打包数 同时给出两个信息: 的个数 ,且 每一类的大小。 有封闭公式。 由 可推出 时 ;只有 时能比"整类涂零"再多省一个 。 - 构造是"按行循环的列余数排列"。 第
行的 放在 ,只要 是 的排列, 块里每行每列每类各一个 ;换排列就是换"多出的行列落在哪一类上"。 种 各配一个排列,离线穷举 个排列定表即可——这正是官方题解建议的"枚举 的构造,本地验证连通性与最大值"。 - 瘦长情形要单列。
时四类被压扁, 不可达: 要 个 , 要 个 , 是最容易踩坑的一档。 - 本题多解,对拍要写 checker。 盘面很小时可以照题意做「命中集 DFS + 连通性检查」算出真实最优值(
brute.cpp),但由于题面允许多解,正解与暴力的输出不会逐字节相同,对拍必须用checker.py做 SPJ:校验输出格式、矩阵合法性(无四个连续同号、四连通、第一行报的个数与矩阵一致)以及最优值。