F. Quests - 题解
比赛与标签
比赛: Codeforces Round 835 (Div. 4) 标签: binary search, greedy, sortings 难度: *1500
题目大意喵~
主人你好呀~ 这道题是这样的:我们有 n
个任务可以做,每个任务 i
完成后能拿到 a_i
个金币。我们总共有 d
天的时间,每天最多只能完成一个任务。
这里有一个特殊的规则哦:如果我们完成了一个任务,那么在接下来的 k
天里都不能再做这个任务了。举个例子,如果 k=2
,我们在第1天做了任务1,那么第2天和第3天就不能再做它了,直到第4天才能再次选择任务1。
我们的目标是,在 d
天内赚到至少 c
个金币。题目想让我们找到一个最大的整数 k
,使得这个目标能够实现。
有几种特殊情况需要注意呐:
- 如果无论
k
是多少,我们都无法赚够c
个金币,就输出Impossible
。 - 如果
k
可以是任意大的正整数(比如我们第一天就赚够了钱,后面根本不需要再做任务),就输出Infinity
。 - 否则,就输出那个最大的
k
啦!
解题思路大解析!
这道题看起来有点复杂,但别担心,我们一步一步来拆解它,喵~
第一步:贪心是最好的策略喵!
要想在 d
天内赚尽可能多的金币,我们肯定希望每天都做当前能做的、奖励最高的任务,对吧?这是一个非常直观的贪心思路。所以,我们第一步要做的就是把所有任务的奖励 a_i
从大到小排个序。这样,我们手里就有了一个按奖励排序的任务列表,随时可以从中挑选最好的任务啦!
第二步:特殊情况要先处理好哦~
在开始复杂的计算之前,我们先把两个最简单的情况处理掉:
"Impossible" 的情况:我们能赚到的金币最多是什么情况呢?当然是
k=0
的时候啦!k=0
意味着没有任何冷却时间,我们可以每天都做那个奖励最高的任务(就是排序后的a[0]
)。如果在这种最理想的情况下,d
天内赚到的金币a[0] * d
都比c
少,那肯定是永远也达不成目标的。所以,如果a[0] * d < c
,直接输出Impossible
就好啦。"Infinity" 的情况:什么时候
k
可以无限大呢?就是当我们根本不需要重复做任何任务的时候。如果我们只用前d
天(如果任务总数n
比d
小,那就是前n
天),每天做一个不同的、奖励最高的任务,就已经能赚够c
个金币了,那之后就不用再管什么冷却时间了,k
可以随便设多大都行。所以,我们计算一下前min(n, d)
个奖励最高的任务的总和,如果这个总和大于等于c
,就可以自信地输出Infinity
啦!
第三步:发现单调性,二分出击!
处理完特殊情况,剩下的就是寻找最大的那个 k
了。我们来观察一下 k
的性质:
假设一个 k
值是可行的(我们能用它赚够 c
个金币)。那么,一个比它更小的 k'
(比如 k-1
)是不是也一定可行呢?答案是肯定的!因为更小的 k
意味着任务的冷却时间更短,我们能更频繁地做那些高奖励的任务,赚到的钱只会多不会少。
这种 “如果 k
可行,那么所有小于 k
的值也都可行” 的性质,就是单调性!一看到这种性质,我们聪明的脑袋里就应该亮起一盏灯:二分答案!
我们可以对 k
进行二分查找。k
的取值范围可以是 0
到 d
(k
比 d
大没有意义,因为在 d
天内根本不会重复任务)。我们来设计一个 check(k)
函数,它用来判断对于给定的 k
,我们最多能赚多少金币,看是否能达到 c
。
第四步:check(k)
函数的设计喵~
check(k)
函数是二分搜索的核心。对于一个给定的 k
,我们来算算 d
天内最多能赚多少钱。
- 任务的冷却周期是
k+1
天。也就是说,我们可以在k+1
天里,每天做一个不同的任务。根据贪心策略,我们肯定会选择奖励最高的前k+1
个任务来轮流做。 - 在
d
天里,我们可以完成d / (k+1)
个完整的周期。 - 每个完整周期,我们都会把前
k+1
个奖励最高的任务做一遍。 - 完成这些完整周期后,还剩下
d % (k+1)
天。在这几天里,我们同样会贪心地选择奖励最高的前d % (k+1)
个任务来做。
为了快速计算这些任务奖励的和,我们可以预处理一个前缀和数组 pref
。pref[i]
存的是排序后前 i
个任务的奖励总和。这样,我们就能在 O(1)
的时间内算出任意前 x
个任务的和啦!
所以,对于一个 k
:
- 一个周期的收益是
pref[k+1]
。(如果k+1 > n
,那就只能做完所有n
个任务,收益是pref[n]
)。 d
天的总收益就是(d / (k+1)) * (一个周期的收益) + pref[d % (k+1)]
。
我们用这个总收益和 c
比较,就可以知道当前的 k
是否可行了。
二分搜索的逻辑:
- 我们在
[0, d+1]
这个范围里二分查找k
。 mid = low + (high - low) / 2
。- 如果
check(mid)
返回true
(意味着用mid
作为冷却天数可以赚够钱),说明mid
是一个可能的答案,我们还想试试看有没有更大的k
也行,所以我们记录下mid
,然后让low = mid + 1
。 - 如果
check(mid)
返回false
,说明mid
太大了,冷却时间太长导致赚不够钱,我们需要减小k
,所以让high = mid - 1
。
最后,我们记录下的那个最大的可行 k
就是答案啦!
代码实现喵~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
void solve() {
long long n, c, d;
std::cin >> n >> c >> d;
std::vector<long long> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// 贪心第一步:将任务奖励从大到小排序,喵~
std::sort(a.rbegin(), a.rend());
// 特殊情况1:不可能完成。
// 如果每天都做奖励最高的任务,d天都凑不够c,那就真的没办法了。
if (a[0] * d < c) {
std::cout << "Impossible\n";
return;
}
// 特殊情况2:k可以无限大。
// 如果只用前 min(n, d) 个不同的高奖励任务就能凑够c,那就不需要重复任务,k可以任意大。
long long sum_first_d = 0;
for (long long i = 0; i < std::min(n, d); ++i) {
sum_first_d += a[i];
}
if (sum_first_d >= c) {
std::cout << "Infinity\n";
return;
}
// 为了快速计算收益,我们预处理前缀和数组 pref
std::vector<long long> pref(n + 1, 0);
for (int i = 0; i < n; ++i) {
pref[i + 1] = pref[i] + a[i];
}
// 开始二分查找 k 的最大值!
long long low = 0, high = d + 1, ans = 0;
while (low <= high) {
long long k = low + (high - low) / 2;
// 一个完整的任务周期是 k+1 天
long long k_plus_1 = k + 1;
// 计算 d 天内有多少个完整的周期
long long num_cycles = d / k_plus_1;
// 计算完整周期后还剩多少天
long long rem_days = d % k_plus_1;
long long total_coins;
// 计算总金币数,注意如果 k+1 > n,一个周期只能做 n 个任务
if (k_plus_1 <= n) {
// 完整周期的收益 + 剩余天数的收益
total_coins = num_cycles * pref[k_plus_1] + pref[rem_days];
} else {
// 任务数不够一个周期,每个周期都是做完所有n个任务
total_coins = num_cycles * pref[n] + pref[std::min(n, rem_days)];
}
// check(k) 的判断
if (total_coins >= c) {
// 这个 k 可行,我们把它存下来,然后尝试更大的 k
ans = k;
low = k + 1;
} else {
// 这个 k 不行,太大了,需要减小 k
high = k - 1;
}
}
std::cout << ans << "\n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
复杂度分析
时间复杂度: O(n log n + log d) 的说。
- 对于每个测试用例,我们首先需要对
n
个任务奖励进行排序,这需要 O(n log n) 的时间。 - 之后我们计算前缀和,需要 O(n) 的时间。
- 核心的二分查找部分,搜索范围是
[0, d+1]
,所以会进行 O(log d) 次迭代。在每次迭代中,check
函数的计算因为有了前缀和,只需要 O(1) 的时间。 - 所以总的时间复杂度是 O(n log n + log d) 呐。
- 对于每个测试用例,我们首先需要对
空间复杂度: O(n) 的说。
- 我们需要一个数组
a
来存储n
个任务的奖励,还需要一个前缀和数组pref
,大小也是n+1
。所以空间复杂度是 O(n)。
- 我们需要一个数组
知识点与总结
这道题是一道非常经典的 “贪心 + 二分答案” 组合题,喵~
贪心策略 (Greedy):遇到求最优解的问题,首先可以想想有没有简单的贪心策略。在这里,“总是选择当前可用的、奖励最高的任务” 就是一个非常有效的贪心策略,它为我们后续的计算奠定了基础。
二分答案 (Binary Search on Answer):当题目要求解一个最大(或最小)值,并且这个值的可行性具有单调性时,二分答案就是我们的不二之选!识别出单调性是解决这类问题的关键。
前缀和优化 (Prefix Sum):在二分的
check
函数中,如果需要反复求一个区间的和,预处理前缀和数组是一种非常常见的优化技巧,能将求和的时间从 O(n) 降到 O(1)。边界处理 (Edge Cases):像
Impossible
和Infinity
这样的特殊情况,在主逻辑开始前单独处理,可以让代码结构更清晰,也能避免在二分过程中出现意想不到的错误。
希望这篇题解能帮助到你,一起加油,成为更厉害的算法大师吧,喵~!