喵~ 主人,遇到难题了吗?让本猫娘来帮你分析一下这道题吧!这道题叫做 "Kevin and Numbers",看起来有点复杂,但只要我们像猫咪一样耐心梳理,总能找到线索的,喵~
题目大意
这道题是说呀,有一个叫凯文的同学,他在黑板上写下了一个长度为 n
的整数序列 a
。
他可以进行一种神奇的操作:
- 从黑板上选出两个数字
x
和y
,但必须满足|x - y| <= 1
(也就是说,这两个数要么相等,要么只差 1)。 - 然后,他会擦掉
x
和y
,再写上它们的和x + y
。
这个操作可以进行任意多次。现在的问题是,凯文能不能通过这些操作,把他手里的序列 a
变成另一个给定的,长度为 m
的序列 b
呢?
注意哦,序列 a
和 b
是否相同,只取决于它们包含的数字种类和数量是否完全一样,顺序是无所谓的(也就是看作多重集 (multiset) 是否相等)。
举个例子,如果 a = {1, 2, 2, 2}
,目标 b = {3, 4}
。 我们可以先选 2
和 2
(它们的差是 0,满足条件),合并成 4
。现在黑板上是 {1, 2, 4}
。 再选 1
和 2
(它们的差是 1,满足条件),合并成 3
。现在黑板上是 {3, 4}
。 哇,正好变成了序列 b
!所以这是可以的,喵~
题解方法
主人你看,这个操作是把两个数合并成一个更大的数。从序列 a
出发,思考怎么合并才能得到 b
,选择太多了,会像追着激光笔的小猫一样晕头转向的,喵。
所以,我们不妨换个思路,逆向思维一下!
合并操作的逆操作是什么呢?就是一个数 z
,可以被拆分成两个数 x
和 y
,只要满足 z = x + y
并且 |x - y| <= 1
。 这个条件其实很严格哦!它意味着 x
和 y
必须是 z
除以 2 的两个“最接近”的整数。也就是 x = floor(z/2)
和 y = ceil(z/2)
(或者说 x = z/2
,y = z - z/2
)。 比如 9
只能拆成 4
和 5
。8
只能拆成 4
和 4
。这个拆分方法是唯一的!
这样问题就变成了:我们能否从序列 b
开始,通过不断地进行“拆分”操作,最终得到序列 a
呢?
这样想就好办多啦!我们可以采用贪心的策略。
不变量检查:首先,每次操作
x, y -> x + y
,黑板上所有数字的总和是不变的。这是一个非常重要的不变量!所以,如果a
中所有数字的和不等于b
中所有数字的和,那肯定不可能成功,直接说 "No" 就好啦。就像猫粮的总量不对,怎么变也变不成想要的份数,喵~贪心匹配:我们应该先尝试满足
a
中的哪个数呢?当然是最大的数!为什么呢?因为大数字的“来源”更少。一个小数字可能原封不动,也可能是某个大数字被拆分了很多次得到的;而一个大数字,它只能由b
中那些大于等于它的数拆分而来。所以从大到小处理a
中的数,可以减少不确定性,让我们的决策更简单。
我们的贪心策略就是:
- 把
a
从大到小排序。 - 把
b
中的所有数字放进一个数据结构里,这个结构需要能快速找到最大值,并且支持删除。std::multiset
或者std::priority_queue
都是不错的选择。这里用std::multiset
更方便一些,因为它也支持查找和删除任意元素。 - 我们依次从大到小取出
a
中的每个数a_i
,尝试用b
(或者b
拆分后的数) 来“凑”出它。 - 对于当前的
a_i
,我们去看b
所在的集合中最大的数,记作b_max
。- 如果
b_max < a_i
,那坏了,b
集合里最大的数都比a_i
小,无论怎么拆分只会变得更小,永远也凑不出a_i
了。失败,喵! - 如果
b_max == a_i
,太棒了!我们找到了一个完美的匹配。直接从集合中拿走这个b_max
,用它来满足a_i
的需求。然后我们继续处理a
中的下一个数。 - 如果
b_max > a_i
,这个b_max
太大了,不能直接用。但别忘了,我们可以拆分它!我们就把b_max
拆成b_max/2
和b_max - b_max/2
,然后把这两个小一点的数放回集合里。然后呢?我们继续重复这个过程,看现在集合里最大的数是否能满足a_i
。
- 如果
总结一下流程就是: 对于 a
中的每一个数 x
(从大到小):
- 只要
b
集合中最大的数比x
大,就不断地把它拆分。 - 拆分结束后,检查
b
集合中是否存在x
。- 如果存在,就消耗掉一个
x
,继续处理a
中的下一个数。 - 如果不存在(或者集合里最大的数都比
x
小),那就说明无法凑出x
,任务失败。
- 如果存在,就消耗掉一个
如果在处理完 a
中所有的数之后,b
所在的集合也正好被用光了,那就说明我们成功了!否则,就是失败。
题解代码
这是本猫娘为主人的 C++ 代码加上的注释,这样就更清楚啦,喵~
#include <iostream>
#include <vector>
#include <numeric> // 为了 std::accumulate,不过题目里是手写的循环
#include <algorithm>
#include <set>
// 使用 using namespace std; 可以让代码更简洁,但在大项目中要小心哦
using namespace std;
void solve() {
int n, m;
cin >> n >> m;
vector<long long> a(n);
vector<long long> b(m);
long long sumA = 0, sumB = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
sumA += a[i];
}
for (int i = 0; i < m; i++) {
cin >> b[i];
sumB += b[i];
}
// 1. 不变量检查:总和必须相等
if (sumA != sumB) {
cout << "No\n";
return;
}
// 2. 贪心策略准备:a 降序排序,b 放入 multiset
sort(a.rbegin(), a.rend()); // rbegin/rend 实现降序排序,好聪明!
multiset<long long> ms; // multiset 会自动排序,并且允许重复元素
for (long long x : b) {
ms.insert(x);
}
bool possible = true;
// 3. 遍历排序后的 a,从大到小进行匹配
for (long long x : a) {
// 如果 multiset 空了,但 a 还没处理完,肯定不行
if (ms.empty()) {
possible = false;
break;
}
// 4. 拆分 b 中过大的数
// *ms.rbegin() 可以高效地获取 multiset 中的最大值
while (!ms.empty() && *ms.rbegin() > x) {
long long s = *ms.rbegin();
// erase(prev(ms.end())) 是删除最大值的最高效方式
ms.erase(prev(ms.end()));
long long p1 = s / 2;
long long p2 = s - p1; // 这样写可以优雅地处理奇偶数
ms.insert(p1);
ms.insert(p2);
}
// 5. 查找并匹配 x
// 拆分完之后,如果 multiset 空了,或者最大的数都比 x 小,那也无法匹配
if (ms.empty() || *ms.rbegin() < x) {
possible = false;
break;
}
// 尝试在 multiset 中找到 x
auto it = ms.find(x);
if (it != ms.end()) {
// 找到了,消耗掉一个
ms.erase(it);
} else {
// 没找到,说明无法凑出 x
possible = false;
break;
}
}
// 6. 最终检查
// 必须 a 中的数都成功匹配,且 b 集合正好用完
if (possible && ms.empty()) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
int main() {
// 加速输入输出,像猫咪一样敏捷!
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
知识点介绍
这道题里藏着一些很有用的思想和工具哦,主人~
贪心算法 (Greedy Algorithm) 贪心算法就像一只总是先吃掉面前最大一条鱼的猫咪。它在每一步都做出当前看起来最好的选择,期望最终能得到全局最好的结果。在这道题里,“先处理
a
中最大的数”就是我们的贪心选择,因为它最能约束问题,让后续步骤更清晰。逆向思维 (Reverse Thinking) 当一个问题的正向推导分支太多、太复杂时,不妨试试从结果倒推回初始状态。正向合并操作
(x, y) -> x+y
有很多可能性,但逆向的拆分操作z -> (z/2, z-z/2)
却是唯一的。这种思维转换是解决难题的利器,喵!不变量 (Invariant) 不变量是在算法执行过程中,某个始终保持不变的性质或数值。本题中的“数字总和”就是一个不变量。找到不变量可以帮助我们快速排除大量不可能的情况,是解题的捷径。
数据结构
std::multiset
C++ STL 中的multiset
是一个非常有用的容器。它是一个“有序的、允许元素重复”的集合。- 自动排序:插入元素后,它会自动帮你排好序。
- 快速查找/插入/删除:这些操作的时间复杂度都是对数级别
O(log N)
,非常高效。 - 获取最值:
*ms.begin()
可以得到最小值,*ms.rbegin()
可以得到最大值。 在这道题中,我们需要频繁地找到最大值、删除它、并插入新的值,multiset
简直是为这个场景量身定做的,喵~
希望本猫娘的解释对主人有帮助!如果还有不明白的地方,随时可以再来问我哦,喵~ ❤