CF1151B Dima and a Bad XOR 题解
题目描述:
Dima and a Bad XOR
题面翻译
题目描述
来自克雷姆兰德的学生迪马有一个大小为 n × m n \times m n×m 的矩阵,其中只包含非负整数。
他希望从矩阵的每一行中选出一个整数,使得所选整数的按位异或严格大于零。
也就是说,他想选择一个整数序列 c 1 , c 2 , … , c n c_1,c_2,\dots,c_n c1,c2,…,cn ( 1 ≤ c j ≤ m ) (1\leq c_j \leq m) (1≤cj≤m) 使得不等式 a 1 , c 1 ⊕ a 2 , c 2 ⋯ ⊕ a n , c n > 0 a_{1,c_1}\oplus a_{2,c_2}\dots \oplus a_{n,c_n} > 0 a1,c1⊕a2,c2⋯⊕an,cn>0成立,其中 a i , j a_{i,j} ai,j 是第 i i i 行和第 j j j 列的矩阵元素。
x ⊕ y x\oplus y x⊕y 表示 x x x 和 y y y 按位异或运算,这里是他的定义。
输入输出格式
输入格式:
第一行包含两个整数 n n n 和 m m m $( 1 \leq n, m \leq 500 ) $ ,分别代表矩阵的行数和列数。
接下来的 n n n 行中的每一行包含 m m m 个整数:第 i i i 行中的第 j j j 个整数是矩阵 a a a 的第 i i i 行的第 j j j 个元素 a i , j ( 0 ≤ a i , j ≤ 1023 ) a_{i,j}\ (0\leq a_{i,j}\leq 1023) ai,j (0≤ai,j≤1023)
输出格式:
如果无法从每一行中选择一个整数,使其按位异或严格大于零,则输出“NIE”。
否则在第一行输出“TAK”,接下来的 n n n 行里,输出 n n n 个整数 c 1 , c 2 , … , c n c_1,c_2,\dots,c_n c1,c2,…,cn ( 1 ≤ c j ≤ m ) (1\leq c_j \leq m) (1≤cj≤m) ,使得不等式 a 1 , c 1 ⊕ a 2 , c 2 ⋯ ⊕ a n , c n > 0 a_{1,c_1}\oplus a_{2,c_2}\dots \oplus a_{n,c_n} > 0 a1,c1⊕a2,c2⋯⊕an,cn>0成立。
如果有多个可能的答案,您可以输出任何答案。
说明
在第一个例子中,矩阵中的所有数字都是0,因此不可能在表的每一行中选择一个数字,以使它们的按位异或严格大于零。
在第二个例子中,所选数字是 7 7 7(第一行中的第一个数字)和 10 10 10(第二行中的第三个数字), 7 ⊕ 10 = 13 7 \oplus 10 = 13 7⊕10=13 , 13 13 13 大于 0 0 0 ,因此找到了答案。
题目描述
Student Dima from Kremland has a matrix $ a $ of size $ n \times m $ filled with non-negative integers.
He wants to select exactly one integer from each row of the matrix so that the bitwise exclusive OR of the selected integers is strictly greater than zero. Help him!
Formally, he wants to choose an integers sequence $ c_1, c_2, \ldots, c_n $ ( $ 1 \leq c_j \leq m $ ) so that the inequality $ a_{1, c_1} \oplus a_{2, c_2} \oplus \ldots \oplus a_{n, c_n} > 0 $ holds, where $ a_{i, j} $ is the matrix element from the $ i $ -th row and the $ j $ -th column.
Here $ x \oplus y $ denotes the bitwise XOR operation of integers $ x $ and $ y $ .
输入格式
The first line contains two integers $ n $ and $ m $ ( $ 1 \leq n, m \leq 500 $ ) — the number of rows and the number of columns in the matrix $ a $ .
Each of the next $ n $ lines contains $ m $ integers: the $ j $ -th integer in the $ i $ -th line is the $ j $ -th element of the $ i $ -th row of the matrix $ a $ , i.e. $ a_{i, j} $ ( $ 0 \leq a_{i, j} \leq 1023 $ ).
输出格式
If there is no way to choose one integer from each row so that their bitwise exclusive OR is strictly greater than zero, print “NIE”.
Otherwise print “TAK” in the first line, in the next line print $ n $ integers $ c_1, c_2, \ldots c_n $ ( $ 1 \leq c_j \leq m $ ), so that the inequality $ a_{1, c_1} \oplus a_{2, c_2} \oplus \ldots \oplus a_{n, c_n} > 0 $ holds.
If there is more than one possible answer, you may output any.
样例 #1
样例输入 #1
3 2
0 0
0 0
0 0
样例输出 #1
NIE
样例 #2
样例输入 #2
2 3
7 7 7
7 7 10
样例输出 #2
TAK
1 3
提示
In the first example, all the numbers in the matrix are $ 0 $ , so it is impossible to select one number in each row of the table so that their bitwise exclusive OR is strictly greater than zero.
In the second example, the selected numbers are $ 7 $ (the first number in the first line) and $ 10 $ (the third number in the second line), $ 7 \oplus 10 = 13 $ , $ 13 $ is more than $ 0 $ , so the answer is found.
题目分析
这是一道很有趣的小题,可以试着一题多解。
首先简单了解一下异或(也写作
x
o
r
,
⊕
xor,\oplus
xor,⊕,C++ 中的 ^
)的几个性质:
- a ⊕ a = 0 a \oplus a = 0 a⊕a=0
- a ⊕ 0 = a a \oplus 0 = a a⊕0=a
- ( a ⊕ b ) ⊕ c = a ⊕ ( b ⊕ c ) (a \oplus b) \oplus c = a \oplus (b \oplus c) (a⊕b)⊕c=a⊕(b⊕c),即异或满足结合律
- a ⊕ b = b ⊕ a a \oplus b = b \oplus a a⊕b=b⊕a,即异或满足交换律
二进制提取出第
t
t
t 位的方法:
x
x
x 的第
y
y
y 位(
y
y
y 从
0
0
0 开始,最右边一位为第
0
0
0 位)二进制
=
=
= (x >> y) & 1
。
同时注意这道题的 n , m ≤ 5000 , a i , j ≤ 1023 n,m \le 5000,a_{i,j} \le 1023 n,m≤5000,ai,j≤1023,说明我们可以放心大胆枚举。
先看我个人想出来的几种思路:
思路 1:
既然异或满足交换律,我们可以先假定全都选每一行的第一个数,如果此时异或和不为 0 0 0,就找到了答案;否则随便替换其中的一个数,就可以使异或和不为 0 0 0 了。这个思路比较简单巧妙。
这样复杂度 O ( n m ) \text{O}(nm) O(nm)
Code: \text{Code:} Code:
//思路一代码
#include <bits/stdc++.h>
using namespace std;
int n , m;
int a[510][510];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
cin >> a[i][j];
}
}
int oplus = 0; //异或和
for(int i = 1; i <= n; i ++){
oplus ^= a[i][1];
}
if(oplus > 0){ //如果此时异或和大于0,直接输出
cout << "TAK\n";
for(int i = 1; i <= n; i ++){
cout << 1 << " ";
}
}else{ //否则遍历每一行每一列,直到找到与该行的第一个不相等的,直接输出
for(int i = 1; i <= n; i ++){
for(int j = 2; j <= m; j ++){
if(a[i][j] != a[i][1]){ //找到了
cout << "TAK\n";
for(int k = 1; k <= n; k ++){
if(k == i) cout << j << " "; //只改变当前行的路径即可
else cout << 1 << " ";
}
return 0; //直接返回
}
}
}
cout << "NIE\n"; //这时候就真不行了
}
return 0;
}
洛谷上 66 66 66 个测试点,总耗时 265 m s 265ms 265ms,最慢在 #30,耗时 265 m s 265ms 265ms;运行时间超过 100 m s 100ms 100ms 的点共 6 6 6 个。但绝大多数点都是 0 m s 0ms 0ms,已经很好了。Link
思路 2:
位运算的常用解题方法就是按位枚举。已知如果最终结果不为 0 0 0,则它的二进制定有一位为 1 1 1,我们可以枚举这个为 1 1 1 的位。
假设位为 1 1 1 的位为第 t t t 位,把 a i , j a_{i,j} ai,j 的二进制第 t t t 位拆出来,单独放到一个数组 b i t bit bit 中。这时演变成了如何在 b i t bit bit 数组中找一条自上而下的路径,使得异或和不为 0 0 0。这时又可以用思路 1 1 1 的方法求解了。
这样复杂度也是 O ( n m ) \text{O}(nm) O(nm)。
Code: \text{Code:} Code:
//思路2代码
#include <bits/stdc++.h>
using namespace std;
int n , m;
int a[510][510];
int bit[510][510]; //枚举时记录a[i][j]的第t位二进制位
int getbit(int x , int y){ //获取x的第y位二进制(从后往前第y位,y从0开始)
return (x >> y) & 1;
}
int main()
{
scanf("%d %d" , &n , &m);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
scanf("%d" , &a[i][j]);
}
}
for(int t = 0; t <= 31; t ++){ //枚举最终结果二进制为1的位
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
bit[i][j] = getbit(a[i][j] , t); //获取每一个a[i][j]的第t位二进制
}
}
int oplus = 0; //异或和
for(int i = 1; i <= n; i ++){ //贪心,先默认取每一行的第一个,不行再枚举往下取
oplus ^= bit[i][1];
}
if(oplus > 0){ //如果此时异或和大于0,直接输出
printf("TAK\n");
for(int i = 1; i <= n; i ++){
printf("1 ");
}
return 0; //直接返回
}else{ //否则遍历每一行每一列,直到找到与该行的第一个不相等的,直接输出
for(int i = 1; i <= n; i ++){
for(int j = 2; j <= m; j ++){
if(bit[i][j] != bit[i][1]){ //找到了
printf("TAK\n");
for(int k = 1; k <= n; k ++){
if(k == i) printf("%d " , j); //只改变当前行的路径即可
else printf("1 ");
}
return 0; //直接返回
}
}
}
}
}
printf("NIE\n"); //这时候就真不行了
return 0;
}
洛谷上 66 66 66 个测试点,总耗时 62 m s 62ms 62ms,最慢在 #10,耗时 46 m s 46ms 46ms;运行时间超过 0 m s 0ms 0ms 的点共 14 14 14 个,但运行时间都在 50 m s 50ms 50ms 以下,比思路 1 表现更好。Link
思路 2 优化:
发现思路 2 2 2 在找路径时浪费了太多时间,又考虑到 b i t bit bit 数组中只有 01 01 01 两种元素,可以考虑压缩数组。
设 f i , j f_{i,j} fi,j 表示第 i i i 行二进制第 t t t 位为 j j j 的元素个数。
补充:奇数次 1 1 1 异或结果定不为 0 0 0。
选数时先选必须选的行,即一行全是 0 0 0 或 1 1 1 的,如果此时选 1 1 1 的个数为奇数,就说明找到了答案,都是 1 1 1 的行就直接选 1 1 1,否则就都选 0 0 0;
否则如果有还存在既有 0 0 0 也有 1 1 1 的行,就可以在其中一行选 1 1 1,其他选 0 0 0,由于 偶数 + 1 = 奇数 \text{偶数}+1=\text{奇数} 偶数+1=奇数,这样也可以完成任务。
这样复杂度还是 O ( n m ) \text{O}(nm) O(nm),和前两个差不多。但要是出题人特意卡暴力的话,这样兴许会好一点。
Code: \text{Code:} Code:
//思路2优化
#include <bits/stdc++.h>
using namespace std;
int n , m;
int a[510][510];
int f[510][2]; //f[i][j]:在第i行,a[i][j]的第t位二进制位为j的个数
#define getbit(x , y) ((x >> y) & 1) //获取x的第y位二进制(从后往前第y位,y从0开始)
int main()
{
scanf("%d %d" , &n , &m);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
scanf("%d" , &a[i][j]);
}
}
for(int t = 0; t <= 31; t ++){ //枚举最终结果二进制为1的位
for(int i = 1; i <= n; i ++){
f[i][0] = f[i][1] = 0; //先清空
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
f[i][getbit(a[i][j] , t)] ++; //按位统计
}
}
int tot1 = 0; //统计当前行只有1的个数,便于后面使用
int tot0 = 0; //统计当前行只有0的个数
int tot01 = 0; //统计当前行01都有的个数
for(int i = 1; i <= n; i ++){
if(f[i][1] == m) tot1 ++;
else if(f[i][0] == m) tot0 ++;
else tot01 ++;
}
if(tot1 % 2 == 1){ //根据异或性质,奇数个1异或和定不为0,这样只选必须选的,可选可不选的全选0
printf("TAK\n");
for(int i = 1; i <= n; i ++){
if(f[i][1] == m || f[i][0] == m){
printf("1 "); //如果当前行全是1或0,选择的数是唯一的,此时输出下标1
}
else{
for(int j = 1; j <= m; j ++){
if(getbit(a[i][j] , t) == 0){ //找第一个0输出,根据异或性质,这样结果不变
printf("%d " , j);
break;
}
}
}
}
return 0; //直接返回
}else if(tot01 > 0){ //这样也可以,把都可以选的行里选上一个1即可
printf("TAK\n");
bool flag = false; //可选不可选的一行是不是选上了
for(int i = 1; i <= n; i ++){
if(f[i][1] == m || f[i][0] == m){
printf("1 "); //同上
}else if(flag == false){ //可选可不选的行里选一个1,因为偶数+1=奇数
for(int j = 1; j <= m; j ++){
if(getbit(a[i][j] , t) == 1){
printf("%d " , j); //输出
flag = true; //更新标记
break;
}
}
}else{ //再不行就找0
for(int j = 1; j <= m; j ++){
if(getbit(a[i][j] , t) == 0){
printf("%d " , j); //输出
break;
}
}
}
}
return 0; //直接返回
}
}
printf("NIE\n"); //这时候就真不行了
return 0;
}
洛谷上 66 66 66 个测试点,总耗时 61 m s 61ms 61ms,最慢在 #9,耗时 61 m s 61ms 61ms;运行时间超过 0 m s 0ms 0ms 的点共 15 15 15 个,但运行时间都在 50 m s 50ms 50ms 以下,和思路 2 2 2 大差不差。Link
下面是我看到题解区中很有趣的做法,
思路 3:
由于异或本身的原因,异或和不为 0 0 0 的几率特别特别大,所以可以多次随机打乱数组,就有近乎 100 % 100\% 100% 的可能得到正确答案。
Code: \text{Code:} Code:
//思路3
#include <bits/stdc++.h>
using namespace std;
int n , m;
pair <int , int> a[510][510];
int main(){
scanf("%d %d" , &n , &m);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
scanf("%d" , &a[i][j].first);
a[i][j].second = j;
}
}
for(int k = 1; k <= 3; k ++){//做少了容易被卡,3次不多不少正正好
for(int i = 1; i <= n; i ++)
random_shuffle(a[i] + 1 , a[i] + m + 1); //每一行都随机打乱
for(int i = 1; i <=m; i ++){
int x = 0;
for(int j = 1; j <= n; j ++)
x ^= a[j][i].first;
if(x > 0){
puts("TAK");
for(int j = 1; j <= n; j ++){
printf("%d " , a[j][i].second);
}
return 0;
}
}
}
puts("NIE");
return 0;
}
洛谷上 66 66 66 个测试点,总耗时 62 m s 62ms 62ms,最慢在 #10,耗时 61 m s 61ms 61ms,也很快!(由于随机的结果有较大的不确定性,故不作具体统计,仅为参考)
random_shuffle
函数的具体用法参考这篇文章。
END
谢谢大家!