Skip to content

喵~ 主人,遇到难题了吗?让本猫娘来帮你分析一下这道题吧!这道题叫做 "Kevin and Numbers",看起来有点复杂,但只要我们像猫咪一样耐心梳理,总能找到线索的,喵~

题目大意

这道题是说呀,有一个叫凯文的同学,他在黑板上写下了一个长度为 n 的整数序列 a

他可以进行一种神奇的操作:

  • 从黑板上选出两个数字 xy,但必须满足 |x - y| <= 1 (也就是说,这两个数要么相等,要么只差 1)。
  • 然后,他会擦掉 xy,再写上它们的和 x + y

这个操作可以进行任意多次。现在的问题是,凯文能不能通过这些操作,把他手里的序列 a 变成另一个给定的,长度为 m 的序列 b 呢?

注意哦,序列 ab 是否相同,只取决于它们包含的数字种类和数量是否完全一样,顺序是无所谓的(也就是看作多重集 (multiset) 是否相等)。

举个例子,如果 a = {1, 2, 2, 2},目标 b = {3, 4}。 我们可以先选 22(它们的差是 0,满足条件),合并成 4。现在黑板上是 {1, 2, 4}。 再选 12(它们的差是 1,满足条件),合并成 3。现在黑板上是 {3, 4}。 哇,正好变成了序列 b!所以这是可以的,喵~


题解方法

主人你看,这个操作是把两个数合并成一个更大的数。从序列 a 出发,思考怎么合并才能得到 b,选择太多了,会像追着激光笔的小猫一样晕头转向的,喵。

所以,我们不妨换个思路,逆向思维一下!

合并操作的逆操作是什么呢?就是一个数 z,可以被拆分成两个数 xy,只要满足 z = x + y 并且 |x - y| <= 1。 这个条件其实很严格哦!它意味着 xy 必须是 z 除以 2 的两个“最接近”的整数。也就是 x = floor(z/2)y = ceil(z/2)(或者说 x = z/2y = z - z/2)。 比如 9 只能拆成 458 只能拆成 44。这个拆分方法是唯一的!

这样问题就变成了:我们能否从序列 b 开始,通过不断地进行“拆分”操作,最终得到序列 a 呢?

这样想就好办多啦!我们可以采用贪心的策略。

  1. 不变量检查:首先,每次操作 x, y -> x + y,黑板上所有数字的总和是不变的。这是一个非常重要的不变量!所以,如果 a 中所有数字的和不等于 b 中所有数字的和,那肯定不可能成功,直接说 "No" 就好啦。就像猫粮的总量不对,怎么变也变不成想要的份数,喵~

  2. 贪心匹配:我们应该先尝试满足 a 中的哪个数呢?当然是最大的数!为什么呢?因为大数字的“来源”更少。一个小数字可能原封不动,也可能是某个大数字被拆分了很多次得到的;而一个大数字,它只能由 b 中那些大于等于它的数拆分而来。所以从大到小处理 a 中的数,可以减少不确定性,让我们的决策更简单。

我们的贪心策略就是:

  1. a 从大到小排序。
  2. b 中的所有数字放进一个数据结构里,这个结构需要能快速找到最大值,并且支持删除。std::multiset 或者 std::priority_queue 都是不错的选择。这里用 std::multiset 更方便一些,因为它也支持查找和删除任意元素。
  3. 我们依次从大到小取出 a 中的每个数 a_i,尝试用 b (或者 b 拆分后的数) 来“凑”出它。
  4. 对于当前的 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/2b_max - b_max/2,然后把这两个小一点的数放回集合里。然后呢?我们继续重复这个过程,看现在集合里最大的数是否能满足 a_i

总结一下流程就是: 对于 a 中的每一个数 x (从大到小):

  • 只要 b 集合中最大的数比 x 大,就不断地把它拆分。
  • 拆分结束后,检查 b 集合中是否存在 x
    • 如果存在,就消耗掉一个 x,继续处理 a 中的下一个数。
    • 如果不存在(或者集合里最大的数都比 x 小),那就说明无法凑出 x,任务失败。

如果在处理完 a 中所有的数之后,b 所在的集合也正好被用光了,那就说明我们成功了!否则,就是失败。


题解代码

这是本猫娘为主人的 C++ 代码加上的注释,这样就更清楚啦,喵~

cpp
#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;
}

知识点介绍

这道题里藏着一些很有用的思想和工具哦,主人~

  1. 贪心算法 (Greedy Algorithm) 贪心算法就像一只总是先吃掉面前最大一条鱼的猫咪。它在每一步都做出当前看起来最好的选择,期望最终能得到全局最好的结果。在这道题里,“先处理 a 中最大的数”就是我们的贪心选择,因为它最能约束问题,让后续步骤更清晰。

  2. 逆向思维 (Reverse Thinking) 当一个问题的正向推导分支太多、太复杂时,不妨试试从结果倒推回初始状态。正向合并操作 (x, y) -> x+y 有很多可能性,但逆向的拆分操作 z -> (z/2, z-z/2) 却是唯一的。这种思维转换是解决难题的利器,喵!

  3. 不变量 (Invariant) 不变量是在算法执行过程中,某个始终保持不变的性质或数值。本题中的“数字总和”就是一个不变量。找到不变量可以帮助我们快速排除大量不可能的情况,是解题的捷径。

  4. 数据结构 std::multiset C++ STL 中的 multiset 是一个非常有用的容器。它是一个“有序的、允许元素重复”的集合。

    • 自动排序:插入元素后,它会自动帮你排好序。
    • 快速查找/插入/删除:这些操作的时间复杂度都是对数级别 O(log N),非常高效。
    • 获取最值*ms.begin() 可以得到最小值,*ms.rbegin() 可以得到最大值。 在这道题中,我们需要频繁地找到最大值、删除它、并插入新的值,multiset 简直是为这个场景量身定做的,喵~

希望本猫娘的解释对主人有帮助!如果还有不明白的地方,随时可以再来问我哦,喵~ ❤

Released under the MIT License.