Skip to content

主人,你好呀~!今天本喵要来讲解一道非常有趣的题目,Codeforces 上的 1857C - Assembly via Minimums。这道题就像一个侦探游戏,我们要从一堆零散的线索中,还原出事情的真相,喵~ 🐾

题目大意

这道题是说呢,有一个叫 Sasha 的人,他有一个由 n 个整数组成的数组 a。他觉得有点无聊,于是就把数组里所有可能的数对 (a_i, a_j)(其中 i < j)都找了出来,然后取了每一对中较小的那个数。

这样,他就得到了一个包含了 n * (n-1) / 2 个元素的全新数组 b

举个例子,如果 Sasha 的数组 a[2, 3, 5, 1],他会计算:

  • min(2, 3) = 2
  • min(2, 5) = 2
  • min(2, 1) = 1
  • min(3, 5) = 3
  • min(3, 1) = 1
  • min(5, 1) = 1

所以他得到的 b 数组就是 [2, 2, 1, 3, 1, 1]

做完这些后,Sasha 又把 b 数组里的元素顺序给随机打乱了。最糟糕的是,他把原来的 a 数组给弄丢了!(真是个迷糊的家伙呐...)

我们的任务就是,根据这个被打乱的 b 数组,帮 Sasha 找回 任何一个 可能的原始数组 a


题解方法

这道题看起来有点棘手,因为数组 b 是打乱的,喵。但是,只要我们稍微动动猫猫的脑筋,就能发现其中的小秘密哦!

让我们先来思考一下,如果原来的数组 a排好序的,会发生什么呢?比如说,我们有一个排好序的数组 a' = [a'_1, a'_2, ..., a'_n],其中 a'_1 ≤ a'_2 ≤ ... ≤ a'_n

现在我们用这个有序的 a' 来生成 b 数组,看看有什么规律:

  1. a'_1 是最小的元素。它和后面所有 n-1 个元素(a'_2, ..., a'_n)配对时,产生的最小值肯定都是 a'_1。所以,在 b 数组里,a'_1 这个值会出现 n-1 次,喵!

  2. a'_2 是第二小的元素。它和 a'_1 配对的最小值是 a'_1(我们已经在第一步里算过啦)。它和它后面剩下的 n-2 个元素(a'_3, ..., a'_n)配对时,产生的最小值就都是 a'_2。所以,b 数组里会有 n-2a'_2

  3. 顺着这个思路想下去,a'_3 会出现 n-3 次,a'_4 会出现 n-4 次,...,一直到 a'_{n-1},它只和 a'_n 配对,所以只会出现 1 次。

  4. 那最大的元素 a'_n 呢?因为它最大,所以在任何 min(a'_i, a'_j)(其中 i < j)的计算中,它都不会是那个最小值。所以 a'_n 的值根本不会出现在 b 数组里!真是个狡猾的家伙,喵~

有了这个发现,解法就豁然开朗啦!我们可以反过来利用这个规律来构造 a 数组。

我们的策略是:

  1. 排序:首先,我们把题目给的 b 数组从小到大排个序。这样,所有相同的值就都聚在一起了,方便我们数数。

  2. 贪心拾取

    • 排序后,b 数组里最小的那个值,毫无疑问就是我们想找的 a'_1。根据我们的规律,它应该出现了 n-1 次。我们把这个值加入到我们的答案数组 a 中,然后直接跳过 b 数组中的 n-1 个元素。
    • 跳过之后,b 数组指针指向的新元素,就应该是 a'_2 啦!它应该出现了 n-2 次。我们再把这个值加入答案 a,然后跳过 n-2 个元素。
    • 我们就像捡豆子一样,一直重复这个过程。第 i 次,我们取一个数加入答案 a,然后跳过 n-1-i 个元素。
  3. 处理最后一个元素

    • 通过上面的步骤,我们可以找到 a'_1, a'_2, ..., a'_{n-1}n-1 个元素。
    • 只剩下最后一个元素 a'_n 了。前面说了,它的值不在 b 里。但是题目说只要我们找到 任何一个 可能的 a 就行。为了让我们的构造 a' 保持有序(即 a'_{n-1} ≤ a'_n),最简单的办法就是让 a'_n 等于 a'_{n-1}。这样肯定满足条件,又简单省事,对吧?喵~

这样,我们就成功地还原出了一个可能的 a 数组啦!


题解 (C++)

下面是这个思路的 C++ 实现代码,本喵加了一些注释,方便主人理解哦。

cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

void solve() {
    int n;
    std::cin >> n;
    // b 数组的大小是 n * (n-1) / 2
    long long m = static_cast<long long>(n) * (n - 1) / 2;
    std::vector<int> b(m);
    for (int i = 0; i < m; ++i) {
        std::cin >> b[i];
    }

    // 喵,先把 b 数组排序,这样所有相同的值就聚在一起啦
    std::sort(b.begin(), b.end());

    std::vector<int> a;
    int current_b_idx = 0; // 用一个指针来追踪我们在 b 数组中的位置

    // 我们要找出 a 的前 n-1 个元素
    for (int i = 0; i < n - 1; ++i) {
        // 当前指针指向的元素就是 a' 中下一个最小的元素
        a.push_back(b[current_b_idx]);

        // a'_{i+1} 在 b 中作为最小值出现了 n-1-i 次。
        // 所以我们要跳过这么多元素,去寻找 a'_{i+2}。
        current_b_idx += (n - 1 - i);
    }

    // 最后一个元素 a'_n 怎么办呢?
    // 它的值不会出现在 b 中,但它必须大于等于 a'_{n-1}。
    // 最简单的做法就是让它等于前一个元素 a'_{n-1},简单又有效,喵~
    // a.back() 就是我们刚刚找到的 a'_{n-1}
    a.push_back(a.back());

    // 输出我们找到的 a 数组
    for (int i = 0; i < n; ++i) {
        std::cout << a[i] << (i == n - 1 ? "" : " ");
    }
    std::cout << "\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. 排序 (Sorting) 这道题的核心就是排序!通过对 b 数组进行排序,我们把一个混乱的、看似无序的集合变成了一个有序的序列。这个操作揭示了元素出现次数的内在规律,是解题的关键一步。就像整理毛线球一样,不整理时乱糟糟,理顺了就很好用啦!

  2. 贪心算法 (Greedy Algorithm) 我们的解法其实是一种贪心策略。在每一步,我们都从 b 数组中选择当前能确定的最小元素作为 a 的一部分。我们相信每一步的局部最优选择(即取 b 中最小的、可用的数作为 a 的下一个元素)能够导向一个全局正确的解。事实证明,这个贪心策略在这里是完全正确的,喵!

  3. 观察与归纳 (Observation and Induction) 解决这道题的真正钥匙是敏锐的观察力。我们通过分析一个有序的 a 数组会产生怎样的 b 数组,然后归纳出其中的规律,再反过来利用这个规律去解决问题。这就像猫猫通过仔细观察,就能知道哪个罐头最好吃一样,是一种非常重要的解决问题的能力,的说!

希望本喵的讲解对主人有帮助哦!下次再见啦,喵~ 💖

Released under the MIT License.