喵~ 主人好呀!这次我们来解决一道非常有趣的题目:B. Corner Twist。这道题就像一个数字魔术,我们要揭开它背后的秘密,哼哼,看我的厉害!
题目大意
这道题是这样的,喵~
我们有两个大小都是 的网格,分别叫做 和 。格子里面的数字只有 这三种,就像猫咪的三种心情一样!
我们可以对网格 进行一种特殊的操作,而且想做多少次都行:
- 在网格里选一个长和宽都至少为 2 的矩形区域。
- 这个矩形有四个角角。我们选其中一对对角线上的角,把它们的值都加上 1。
- 剩下没被选中的另一对对角线上的角,就把它们的值都加上 2。
所有的加法都是在模 3 的意义下进行的哦!也就是说,一个数加了之后如果变成了 3,它就会变回 0;如果是 4,就会变成 1。
问题就是:我们能不能通过这些操作,把网格 变得和网格 一模一样呢?如果可以,就告诉人家 "YES",不行就说 "NO",很简单吧,喵~
解题方法
直接一步步模拟操作肯定是不行的啦,太慢了,会超时的说。这种时候,我们就要开动小脑筋,寻找操作中隐藏的规律,也就是所谓的 “不变量”!
让我们仔细分析一下这个神奇的操作,喵。
假设我们选择了一个左上角为 (r1, c1)
,右下角为 (r2, c2)
的矩形。操作会改变四个角的值:a[r1][c1]
, a[r1][c2]
, a[r2][c1]
, a[r2][c2]
。
有两种可能的变化情况(所有计算都在模 3 意义下):
a[r1][c1]
和a[r2][c2]
加 1,同时a[r1][c2]
和a[r2][c1]
加 2。a[r1][c1]
和a[r2][c2]
加 2,同时a[r1][c2]
和a[r2][c1]
加 1。
现在,让我们来观察一下每一行和每一列的总和在操作后会发生什么变化。这可是解题的关键所在哦!
对行的影响
对于任意一行 i
:
- 如果这一行
i
不在矩形的上下边界上(即r1 < i < r2
),那么它完全不受影响,总和当然不变。 - 如果这一行
i
正好是矩形的上边界(i = r1
),那么a[r1][c1]
和a[r1][c2]
的值会被改变。它们增加的值,无论是+1
和+2
,还是+2
和+1
,加起来都是1 + 2 = 3
。在模 3 的世界里,3
就等于0
!所以,这一行的总和在模 3 意义下,其实没有改变! - 同理,如果这一行
i
是下边界(i = r2
),其总和的变化也是1 + 2 = 3
,模 3 后还是0
。
结论一: 无论我们怎么操作,任何一行的所有元素之和,在模 3 意义下都是一个不变量!
对列的影响
- 和行的分析一模一样,喵~ 如果某一列
j
处在矩形的左右边界上,那么它上面两个被影响的元素值的总变化量也是1 + 2 = 3
,模 3 之后等于0
。
结论二: 无论我们怎么操作,任何一列的所有元素之和,在模 3 意义下也是一个不变量!
找到这两个不变量,问题就迎刃而解啦!
如果网格 可以变成网格 ,那么它们必须始终遵守这两个不变的性质。也就是说,对于最终状态 和初始状态 ,必须满足:
- 对于任意一行
i
,a
的第i
行元素之和mod 3
必须等于b
的第i
行元素之和mod 3
。 - 对于任意一列
j
,a
的第j
列元素之和mod 3
必须等于b
的第j
列元素之和mod 3
。
只要检查这两个条件就好啦!如果 和 都满足这两个条件,我们就可以认为 能变成 。反之,只要有任何一行或一列不满足,就绝对不可能变过去,直接回答 "NO" 就好啦,喵~
题解
下面就是代码实现啦,主人请看~
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
void solve() {
int n, m;
std::cin >> n >> m;
// 用 vector 来存网格,就像猫咪的小肉垫一样柔软好用~
std::vector<std::vector<int>> a(n, std::vector<int>(m));
for (int i = 0; i < n; ++i) {
std::string row_str;
std::cin >> row_str;
for (int j = 0; j < m; ++j) {
a[i][j] = row_str[j] - '0'; // 把字符'0','1','2'变成数字0,1,2
}
}
std::vector<std::vector<int>> b(n, std::vector<int>(m));
for (int i = 0; i < n; ++i) {
std::string row_str;
std::cin >> row_str;
for (int j = 0; j < m; ++j) {
b[i][j] = row_str[j] - '0';
}
}
// 检查每一行,喵~
for (int i = 0; i < n; ++i) {
int row_sum_a = 0;
int row_sum_b = 0;
for (int j = 0; j < m; ++j) {
row_sum_a += a[i][j];
row_sum_b += b[i][j];
}
// 如果 a 和 b 对应行的和模 3 后不相等,那肯定不行啦!
if (row_sum_a % 3 != row_sum_b % 3) {
std::cout << "NO\n";
return; // 直接结束,告诉主人不行
}
}
// 检查每一列,喵~
for (int j = 0; j < m; ++j) {
int col_sum_a = 0;
int col_sum_b = 0;
for (int i = 0; i < n; ++i) {
col_sum_a += a[i][j];
col_sum_b += b[i][j];
}
// 如果 a 和 b 对应列的和模 3 后不相等,也肯定不行!
if (col_sum_a % 3 != col_sum_b % 3) {
std::cout << "NO\n";
return; // 同样直接结束
}
}
// 所有检查都通过了,说明 a 可以变成 b,太棒啦!
std::cout << "YES\n";
}
int main() {
// 加速输入输出,让程序跑得像猫一样快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
知识点介绍
主人,做完题也要学习新知识才能进步呀,就像猫咪每天都要磨爪子一样,喵~
1. 模运算 (Modular Arithmetic)
这道题的核心就是模 3 运算。模运算就像一个只有 0, 1, 2 三个数字的时钟。2+1=3
,在时钟上转了一圈又回到了 0
,所以 3 % 3 = 0
。2+2=4
,转了一圈还多一步,就到了 1
,所以 4 % 3 = 1
。 在题目里,对角线上的数字分别加 1 和 2,总和的变化是 1+2=3
。在模 3 的世界里,3
和 0
是等价的。正是这个性质,才让我们找到了解题的突破口呢!
2. 不变量 (Invariants)
不变量是算法竞赛中一个超级重要的概念!它指的是在进行一系列操作的过程中,某个属性或数值始终保持不变。就像不管猫咪怎么打滚,它还是那只可爱的猫咪一样,喵~ 在这道题里,我们发现**“任意一行的元素和模 3 的值”和“任意一列的元素和模 3 的值”**就是两个不变量。无论我们进行多少次题目中定义的操作,这两个值都不会改变。
寻找不变量是一种非常强大的解题策略。当题目问“是否可能”时,可以先思考:如果可能,那么初始状态和目标状态之间必须有什么共同点?这个共同点往往就是不变量。如果连这个共同点都不满足,那答案肯定是“不可能”啦。
3. 必要条件与充分条件
我们找到的这个“行列和模 3 相等”的性质,是一个必要条件。也就是说,要想 能变成 ,必须满足这个条件。而这道题比较特殊,这个必要条件恰好也是充分条件(虽然我们没有严格证明这一点,但在竞赛中通常可以相信题目的设计)。充分条件的意思是,只要满足了这个条件,就一定能从 变成 。
所以,我们只需要检查这个条件,就能得到正确的答案。这大大简化了问题,不用去思考具体怎么操作啦,是不是很巧妙呢?
好啦,这次的题解就到这里啦!希望对主人有帮助,喵~ 下次再一起玩耍吧!