喵~ 主人,今天我们来解决一道关于分礼物的有趣问题吧!这道题叫做 "Gifts Fixing",听起来就很可爱,对不对?呐,就让本喵带你一步步分析,把这个问题轻松解决掉!
B. Gifts Fixing (Codeforces 1399B)
题目大意喵
主人,想象一下,我们有 n
份礼物要送给小朋友们。为了公平,我们希望最后所有的礼物都变得一模一样!
每份礼物 i
里面有 a_i
颗糖果和 b_i
颗橘子。
为了调整礼物,我们可以进行一些操作,每次操作可以选择一份礼物 i
,然后:
- 吃掉一颗糖果(
a_i
减 1)。 - 吃掉一个橘子(
b_i
减 1)。 - 同时吃掉一颗糖果和一个橘子(
a_i
和b_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
对于所有的 i
target_b <= b_i
对于所有的 i
为了让总操作次数最少,我们应该让 target_a
和 target_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_a
和 min_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. 最终算法
我们的算法就非常清晰啦:
- 读入所有礼物的糖果数
a
和橘子数b
。 - 找到所有
a_i
中的最小值min_a
和所有b_i
中的最小值min_b
。 - 初始化总操作次数
total_moves
为 0。 - 遍历每一份礼物
i
,计算max(a[i] - min_a, b[i] - min_b)
,并累加到total_moves
中。 - 最后输出
total_moves
就好啦!
是不是很简单呢?喵~
题解代码
这是本喵为主人准备好的 C++ 代码,加了详细的注释,方便主人理解哦~
#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;
}
知识点小课堂
这道题虽然简单,但背后也藏着一些重要的思想哦,本喵来给你总结一下:
贪心算法 (Greedy Algorithm)
- 贪心算法的核心思想是“目光短浅”,在每一步都做出当前看起来最优的选择,并期望最终能得到全局最优解。
- 在这道题里,我们的“贪心”选择是:确定最终目标时,让糖果和橘子的数量尽可能多(即取所有礼物中的最小值
min_a
和min_b
)。这个选择是正确的,因为它直接最小化了每一步需要“吃掉”的总量,从而保证了总操作数最小。
数学思维与简化
- 将一个看似复杂的操作计数问题
min(da, db) + abs(da - db)
简化为max(da, db)
,是解题的关键一步。这不仅让思路更清晰,也让代码更简洁。在算法竞赛中,发现并利用这种数学上的等价关系,往往能事半功倍!
- 将一个看似复杂的操作计数问题
最值问题
- 寻找数组中的最小值是一个非常基础但重要的问题。无论是
std::min_element
还是手动遍历,都是必须掌握的基本功。这个问题的第一步就是找到这个最小值,为后续的计算奠定基础。
- 寻找数组中的最小值是一个非常基础但重要的问题。无论是
好啦,今天的讲解就到这里啦!主人有没有觉得豁然开朗呢?只要跟着本喵的思路,再难的问题也能迎刃而解的喵~ 下次再见!