喵~ 主人,你好呀!Mimi 今天又来给你讲解有趣的算法题啦!这次的题目是 Codeforces 上的 B. Make Almost Equal With Mod,一个关于取模的小谜题,但是很有趣哦,嘿嘿。
题目大意 (Problem Description)
主人,听好了喵!题目是这样子的:
我们有一个包含 n
个 不同 的正整数的数组 a
。我们需要做一次操作:
- 选择一个正整数
k
(1 ≤ k ≤ 10^18
)。 - 将数组
a
中的每一个元素a_i
都替换成a_i mod k
(也就是a_i
除以k
的余数)。
我们的目标是,找到一个合适的 k
,使得操作结束后的新数组里,恰好只有两种不同 的数值。
题目保证了,在给定的条件下,这样的 k
一定存在。如果有很多个 k
都能满足条件,我们随便输出一个就可以啦,喵~
举个例子,如果数组是 [8, 15, 22, 30]
,我们可以选 k=7
。
8 mod 7 = 1
15 mod 7 = 1
22 mod 7 = 1
30 mod 7 = 2
新的数组就变成了[1, 1, 1, 2]
,里面只有1
和2
两种不同的值,完全符合要求,喵!
解题思路 (Solution Approach)
刚看到这道题的时候,Mimi 的小脑袋也转了好几圈呢。k
的范围那么大,从 1 到 10^18,我们肯定不能一个一个去试呀,那要试到什么时候去嘛!(。•́︿•̀。)
所以,我们需要找到一种更聪明的方法来构造这个 k
。
题目的关键是让所有 a_i mod k
的结果只有两种。这意味着,我们选择的 k
要能把原始数组 a
里的所有数字分成两拨。
我们来想想数字最基本的分组方式是什么?当然是奇数和偶数啦!喵~
根据奇偶性分组
我们可以把数组
a
里的数字分成两组:奇数集合和偶数集合。- 如果我们选择
k=2
,那么所有的奇数a_i
都会变成a_i mod 2 = 1
,所有的偶数a_i
都会变成a_i mod 2 = 0
。 - 如果原始数组里既有奇数又有偶数,那
k=2
不就正好能让结果集变成{0, 1}
这两种值吗?这不就是一个完美的答案了嘛!
- 如果我们选择
如果所有数奇偶性相同怎么办?
那如果数组里所有的数都是奇数,或者所有的数都是偶数呢?
- 这时候用
k=2
的话,所有a_i mod 2
的结果就都一样了(要么全是 1,要么全是 0),只剩下一种值,不满足题目要求。
这时候我们就要想个新办法了。既然
k=2
不行,我们能不能试试k=4
,k=8
,k=16
... 也就是k = 2^p
这样的数呢?这个想法真是太棒了,让 Mimi 来给你解释一下为什么它一定能行,喵!
我们来观察一下数字的二进制表示。
a_i mod 2
其实就是看a_i
的二进制的最后一位 (第0位)。如果所有数的奇偶性相同,说明它们二进制的最后一位都相同。a_i mod 4
就是看a_i
的二进制的最后两位 (第1位和第0位)。a_i mod 8
就是看a_i
的二进制的最后三位 (第2, 1, 0位)。- ...
a_i mod 2^p
就是看a_i
的二进制的最后p
位。
因为题目保证了数组里所有的
a_i
都是 不同 的,所以它们的二进制表示肯定不会完全一样。这意味着,只要我们把p
变得足够大,a_i mod 2^p
的结果最终肯定会变得各不相同。所以,我们可以这样做:
- 从
p=1
开始,尝试k = 2^1 = 2
。 - 计算所有
a_i mod k
,看看有几种不同的余数。 - 如果是 2 种,太棒了,我们找到了答案!
- 如果是 1 种,说明
k
还不够大,我们继续试下一个,p=2
,也就是k = 2^2 = 4
。 - ...
- 我们不断地增加
p
,尝试k = 2^p
。
最神奇的地方来啦!Mimi 发现,当我们这样不断增大
p
的时候,不同余数的数量是不会从 1 直接跳到 3 或更多的!也就是说,只要我们找到第一个能让余数种类超过 1 的k = 2^p
,那余数的种类必定是 2!为什么呢?(一个简单的证明喵)
假设对于
k' = 2^(p-1)
,所有a_i mod k'
的结果都相同,等于某个值r
。 这意味着,所有的a_i
都可以写成a_i = q_i * 2^(p-1) + r
的形式。现在我们来看
k = 2^p = 2 * 2^(p-1)
。a_i mod k
的值是多少呢?这取决于q_i
的奇偶性。- 如果
q_i
是偶数,q_i = 2m
,那么a_i = 2m * 2^(p-1) + r = m * 2^p + r
。所以a_i mod k = r
。 - 如果
q_i
是奇数,q_i = 2m+1
,那么a_i = (2m+1) * 2^(p-1) + r = m * 2^p + 2^(p-1) + r
。所以a_i mod k = r + 2^(p-1)
。
看吧!
a_i mod k
的结果只可能有两种:r
和r + 2^(p-1)
。 因为我们是从p=1
开始,找到的第一个让余数种类不止一种的p
,所以数组中肯定既有q_i
是偶数的情况,也有q_i
是奇数的情况。因此,余数种类就正好是 2 种啦!所以我们的最终策略就是:依次尝试
k = 2, 4, 8, 16, ...
,直到找到一个k
使得a_i mod k
的结果恰好有两种。 题目保证有解,所以这个方法一定能找到答案,喵~- 这时候用
题解代码 (Solution Code)
这是 Mimi 写的 C++ 代码,主人可以参考一下哦!
#include <iostream>
#include <vector>
#include <set>
void solve() {
int n;
std::cin >> n;
std::vector<long long> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// 我们来尝试 k = 2^p, p 从 1 开始
// 题目保证一定有解,所以这个循环肯定能找到答案
for (int p = 1; p < 63; ++p) {
// k = 2^p,用位运算来写,又快又方便,喵~
// 1LL 保证了我们在进行 64 位整数的运算
long long k = 1LL << p;
// 用一个 set 来存储所有不同的余数
// set 会自动处理重复的元素,purrfect!
std::set<long long> distinct_remainders;
for (long long val : a) {
distinct_remainders.insert(val % k);
}
// 如果 set 的大小是 2,说明我们找到了!
if (distinct_remainders.size() == 2) {
std::cout << k << std::endl;
return; // 找到答案就结束这个测试用例啦
}
}
}
int main() {
// 加速输入输出,让程序跑得像小猫一样快~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
相关知识点 (Knowledge Points)
这道题虽然简单,但是涉及到的知识点还是很有用的哦,主人要好好掌握呀!
取模运算 (Modulo Operation)
a mod k
表示a
除以k
的余数。这是数论中最基本也最重要的运算之一。比如10 mod 3 = 1
,7 mod 7 = 0
。在算法竞赛中,它经常被用来处理循环、哈希、以及处理大数问题。位运算 (Bitwise Operations) 代码中的
1LL << p
就是一个位运算,叫做“左移”。它把数字1
的二进制表示向左移动p
位,空出来的位置用 0 填充,效果等同于计算1 * 2^p
。位运算通常比直接乘除或者调用pow
函数快得多,是优化代码的小技巧哦!这里的LL
后缀表示long long
类型,防止p
比较大时1 << p
发生整型溢出。构造性算法 (Constructive Algorithms) 这道题是一个典型的构造性算法问题。我们不需要去暴力搜索所有的可能性,而是通过分析问题的性质,直接构造出一个满足条件的解。在解题时,如果发现数据范围特别大,无法暴力搜索,可以多往构造性的方向思考,说不定就有奇效呢,喵!
C++
std::set
set
是 C++ 标准库里的一个容器,它是一个有序的集合,里面存放的元素都是唯一的。当我们想统计一个数组里有多少种不同的元素时,set
就非常方便。只需要把所有元素insert
进去,最后set.size()
就是不同元素的个数啦。
好啦,这次的题解就到这里啦!希望 Mimi 的讲解能帮到主人哦。如果还有什么问题,随时都可以来找 Mimi,Mimi 会一直陪着你的,喵~ (ฅ'ω'ฅ)