合法矩阵最小修改次数
题目描述
小红定义一个 01 矩阵是合法的,当且仅当其中每个由 0 组成的四连通块大小都恰好为 3,并且每个由 1 组成的四连通块大小也都恰好为 3。
现在给定一个大小为 $2 \times n$ 的 01 矩阵。你每次可以修改一个字符,将 1 改成 0,或将 0 改成 1。
请你求出,最少需要修改多少个字符,才能使这个矩阵变成合法矩阵。如果无论如何都无法变成合法矩阵,输出 $-1$。
这里的连通块采用四连通定义,也就是说两个格子只有在共边时才算相邻。
输入描述
第一行输入一个整数 $n$($1 \le n \le 2 \times 10^5$)。
接下来两行,每行输入一个长度为 $n$ 的 01 字符串,表示矩阵的两行。
输出描述
输出一个整数,表示最少修改次数。
如果无解,输出 $-1$。
示例
输入
1
2
3
6
110110
100100
输出
1
0
说明
原矩阵本身已经合法。其中左侧的 3 个 1 构成一个大小为 3 的连通块,右侧的 3 个 1 也构成一个大小为 3 的连通块。所有 0 的连通块大小同样都恰好为 3,因此不需要修改。
分析与思路
初步探路
看到这道题,我最初尝试对其进行转换:将 1 映射为 1,将 0 映射为 $-1$。为什么这么做?这来源于 Codeforces Round 1097 (Div. 2, Based on Zhili Cup 2026) 的一道 C 题,不过这其实和本题没什么关系。
首先很容易知道 $3 \mid n$(即 $n$ 必须是 3 的倍数),否则无解。接下来需要讨论合法连通块的形状。最初我遗漏了一个重要情况——横着的 111 或者 000 也是符合要求的连通块,这很关键。我当时只考虑了 L 型块,但事实上横排也是合法的。
如果只考虑 L 型,我们会陷入一个误区:认为每一列的和必然是 $-1, 0, 1, -1, 0, 1, \dots$ 或者 $1, 0, -1, 1, 0, -1, \dots$ 这样循环的模式。但这样的推理会遗漏很多可行的情况,因为它无法处理每一列都是 0 的情况。例如,上层是 111000 而下层是 000111,根据上述映射规则和推论会得到错误的答案。核心原因就在于我们忽略了横排摆放也是合法的。
补全思考
明确了连通块可以呈 “L” 型或 “一” 型(横条)后,之前的映射法则显然不再适用。不过之前的思路也并非毫无用处:由于并不存在真正独立的块,我们仍然可以从第一个 $2 \times 3$ 块入手。但很快会发现,只考虑一个块对我们没有任何帮助,我们需要考虑 两个块 之间的关系。为什么呢?还记得”一”型的情况吗?在这种形状下,天然就需要考虑两个 $2 \times 3$ 块之间的拼接。
如果不考虑左右边界,一个 $2 \times 3$ 块总共有以下 6 种合法的形态:
a1: `110` / `100` a2: `100` / `110`
b1: `011` / `001` b2: `001` / `011`
c1: `111` / `000` c2: `000` / `111`
注意,这里只用了三个字母来命名。为什么会这样分组?先思考一下相邻块的约束关系。
根据约束规则,从第一个块出发:
- 如果第一个块是 a1,那么第二个块只能是 a1 或 a2;
- 如果第一个块是 a2,第二个块也只能是 a1 或 a2(a1 和 a2 本质上是上下翻转的关系,根据对称性可快速得到)。
- 同理,b1 后面只能是 b1 或 b2,b2 后面只能是 b1 或 b2。
- 而对于 c1 和 c2,c1 后面只能是 c2,c2 后面只能是 c1(必须交替出现)。
由此可知,整个 $2 \times n$ 矩阵只能由以下三种模式之一构成:
- a 模式:a1 和 a2 的混合(每个 $2 \times 3$ 块独立选择 a1 或 a2);
- b 模式:b1 和 b2 的混合(每个 $2 \times 3$ 块独立选择 b1 或 b2);
- c 模式:c1 和 c2 交替出现(不能混合,必须严格交替)。
因此,我们可以分别枚举每个 $2 \times 3$ 块变成上述某种合法形态所需的修改次数,然后取总和最小的方案。对于 a 模式和 b 模式,每个 $2 \times 3$ 块只需在两种候选形态中取修改次数更少者;对于 c 模式,则需按交替规则计算总修改代价。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
string s1, s2;
cin >> s1 >> s2;
if (n % 3) {
cout << -1 << '\n';
return 0;
}
auto calc = [&](int left, const string& top, const string& bot) {
int res = 0;
for (int k = 0; k < 3; ++k) {
if (s1[left + k] != top[k]) res++;
if (s2[left + k] != bot[k]) res++;
}
return res;
};
int m = n / 3;
int costA = 0, costB = 0, costC0 = 0, costC1 = 0;
for (int i = 0; i < m; ++i) {
int l = i * 3;
int a1 = calc(l, "110", "100");
int a2 = calc(l, "100", "110");
costA += min(a1, a2);
int b1 = calc(l, "011", "001");
int b2 = calc(l, "001", "011");
costB += min(b1, b2);
int c1 = calc(l, "111", "000");
int c2 = calc(l, "000", "111");
if (i % 2 == 0) {
costC0 += c1;
costC1 += c2;
} else {
costC0 += c2;
costC1 += c1;
}
}
int ans = min({costA, costB, costC0, costC1});
cout << ans << '\n';
return 0;
}