Skip to content

喵~ 主人,今天我们来解决一道关于分礼物的有趣问题吧!这道题叫做 "Gifts Fixing",听起来就很可爱,对不对?呐,就让本喵带你一步步分析,把这个问题轻松解决掉!

B. Gifts Fixing (Codeforces 1399B)


题目大意喵

主人,想象一下,我们有 n 份礼物要送给小朋友们。为了公平,我们希望最后所有的礼物都变得一模一样!

每份礼物 i 里面有 a_i 颗糖果和 b_i 颗橘子。

为了调整礼物,我们可以进行一些操作,每次操作可以选择一份礼物 i,然后:

  1. 吃掉一颗糖果(a_i 减 1)。
  2. 吃掉一个橘子(b_i 减 1)。
  3. 同时吃掉一颗糖果和一个橘子(a_ib_i 都减 1)。

当然啦,我们不能吃掉不存在的糖果或橘子,所以它们的数量不能变成负数。

我们的目标是让所有礼物都“相等”。也就是说,经过一系列操作后,所有礼物的糖果数量都相等 (a_1 = a_2 = ... = a_n),并且所有礼物的橘子数量也都相等 (b_1 = b_2 = ... = b_n)。

那么问题来了,最少需要多少次操作才能完成这个任务呢?


解题思路的说

这个问题看起来有点复杂,但其实只要我们抓住关键点,就会变得非常简单喵~

1. 最终的礼物应该是什么样子?

我们的目标是让所有礼物的糖果数变成同一个值,橘子数也变成同一个值。我们把这个最终形态称为 (target_a, target_b)

注意到我们的操作只能减少糖果和橘子的数量,不能增加。这意味着,对于任何一份礼物 i,操作结束后的糖果数 target_a 必须小于或等于它原来的糖果数 a_i。同理,target_b 也必须小于或等于 b_i

target_a <= a_i 对于所有的 itarget_b <= b_i 对于所有的 i

为了让总操作次数最少,我们应该让 target_atarget_b 尽可能大,这样需要减掉的数量就最少了。那么,target_a 最大能取到多少呢?当然是所有 a_i 中的最小值啦!如果 target_a 比任何一个 a_i 的最小值还大,那那个最小的礼物就永远也达不到目标了。

所以,最终的目标形态就是:

  • target_a = min(a_1, a_2, ..., a_n)
  • target_b = min(b_1, b_2, ..., b_n)

我们把它们记作 min_amin_b

2. 如何计算每一份礼物的最少操作次数?

现在我们知道了目标,对于每一份礼物 i,我们需要把它从 (a_i, b_i) 变成 (min_a, min_b)

需要减少的糖果数量是 da = a_i - min_a。 需要减少的橘子数量是 db = b_i - min_b

我们有三种操作。其中,“同时吃掉一颗糖果和一个橘子”这个操作最高效,因为它一次能完成两项任务!所以,我们应该尽可能多地使用这个操作。

  • 我们可以进行 min(da, db) 次“同时吃”的操作。这样,糖果和橘子中需要减少得较少的那一项就完成了。
  • 进行了 min(da, db) 次操作后,我们还需要单独吃掉剩下的糖果或橘子。剩下的数量是 da - min(da, db)db - min(da, db)。这两个差值中,一个必然是 0,另一个是 abs(da - db)。所以还需要 abs(da - db) 次单独吃的操作。

因此,对于礼物 i,总操作次数是 min(da, db) + abs(da - db)

3. 一个神奇的简化!

min(x, y) + abs(x - y) 这个式子,其实可以简化哦!

  • 如果 x >= y,那么 min(x, y) = y, abs(x - y) = x - y。加起来就是 y + (x - y) = x
  • 如果 y > x,那么 min(x, y) = x, abs(x - y) = y - x。加起来就是 x + (y - x) = y

看到了吗喵?min(x, y) + abs(x - y) 其实就等于 max(x, y)

所以,对于每一份礼物 i,把它变成目标形态所需要的最少操作次数就是 max(a_i - min_a, b_i - min_b)

4. 最终算法

我们的算法就非常清晰啦:

  1. 读入所有礼物的糖果数 a 和橘子数 b
  2. 找到所有 a_i 中的最小值 min_a 和所有 b_i 中的最小值 min_b
  3. 初始化总操作次数 total_moves 为 0。
  4. 遍历每一份礼物 i,计算 max(a[i] - min_a, b[i] - min_b),并累加到 total_moves 中。
  5. 最后输出 total_moves 就好啦!

是不是很简单呢?喵~


题解代码

这是本喵为主人准备好的 C++ 代码,加了详细的注释,方便主人理解哦~

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

// 这个函数用来解决单个测试用例喵
void solve() {
    int n;
    std::cin >> n;
    std::vector<long long> a(n);
    std::vector<long long> b(n);

    // 读入所有礼物的糖果数量
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
    // 读入所有礼物的橘子数量
    for (int i = 0; i < n; ++i) {
        std::cin >> b[i];
    }

    // 为了让操作次数最少,我们必须把所有礼物都减少到同一个目标。
    // 因为只能减少,所以目标值不能超过初始值。
    // 为了让减少的量(也就是操作数)最少,我们应该选择尽可能大的目标值。
    // 所以,糖果的目标值就是所有 a_i 中的最小值。
    // 橘子的目标值就是所有 b_i 中的最小值。
    long long min_a = *std::min_element(a.begin(), a.end());
    long long min_b = *std::min_element(b.begin(), b.end());

    long long total_moves = 0;
    // 对每一份礼物,计算把它变成目标 (min_a, min_b) 的最少操作次数
    for (int i = 0; i < n; ++i) {
        // 需要减少的糖果数是 da = a[i] - min_a
        // 需要减少的橘子数是 db = b[i] - min_b
        // 我们优先使用“同时减少”的操作,可以进行 min(da, db) 次。
        // 之后,还需要单独减少 abs(da - db) 次。
        // 总操作数就是 min(da, db) + abs(da - db),这正好等于 max(da, db)!
        // 所以我们直接加上这个最大值就好啦,喵~
        total_moves += std::max(a[i] - min_a, b[i] - min_b);
    }

    std::cout << total_moves << "\n";
}

int main() {
    // 竞赛中常用的快速输入输出,让程序跑得更快一点
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t; // 有 t 组测试数据
    while (t--) {
        solve();
    }

    return 0;
}

知识点小课堂

这道题虽然简单,但背后也藏着一些重要的思想哦,本喵来给你总结一下:

  1. 贪心算法 (Greedy Algorithm)

    • 贪心算法的核心思想是“目光短浅”,在每一步都做出当前看起来最优的选择,并期望最终能得到全局最优解。
    • 在这道题里,我们的“贪心”选择是:确定最终目标时,让糖果和橘子的数量尽可能多(即取所有礼物中的最小值 min_amin_b)。这个选择是正确的,因为它直接最小化了每一步需要“吃掉”的总量,从而保证了总操作数最小。
  2. 数学思维与简化

    • 将一个看似复杂的操作计数问题 min(da, db) + abs(da - db) 简化为 max(da, db),是解题的关键一步。这不仅让思路更清晰,也让代码更简洁。在算法竞赛中,发现并利用这种数学上的等价关系,往往能事半功倍!
  3. 最值问题

    • 寻找数组中的最小值是一个非常基础但重要的问题。无论是 std::min_element 还是手动遍历,都是必须掌握的基本功。这个问题的第一步就是找到这个最小值,为后续的计算奠定基础。

好啦,今天的讲解就到这里啦!主人有没有觉得豁然开朗呢?只要跟着本喵的思路,再难的问题也能迎刃而解的喵~ 下次再见!

Released under the MIT License.