D. Yet Another Monster Killing Problem - 题解
比赛与标签
比赛: Educational Codeforces Round 76 (Rated for Div. 2) 标签: binary search, data structures, dp, greedy, sortings, two pointers 难度: *1700
喵喵,来读题啦!
主人,你好呀!今天我们来挑战一个有趣的怪物杀手问题,喵~
是这样的,我们有一个地下城,里面有 n
只怪物排成一队,每只怪物都有一个力量值 a[i]
。而我们呢,拥有一支由 m
位英雄组成的强大队伍!每位英雄也有自己的力量 p
和耐力 s
。
我们的目标是打败所有怪物,而且要用最少的天数!每天的流程是这样的:
- 我们从英雄队伍里选 一位 英雄出战。
- 这位英雄会从当前还没被打败的第一只怪物开始挑战。
- 要打败一只怪物,英雄的力量
p
必须 大于或等于 怪物的力量a
。如果英雄不够强,他就会撤退,这一天就结束了。 - 打败一只怪物后,英雄可以选择继续挑战下一只,或者离开。
- 英雄在一天内最多只能打败
s
只怪物(也就是他的耐力值),或者打到没有怪物为止,然后这一天就结束了。
那么问题来了,最少需要多少天才能把所有怪物都清理干净呢?如果不可能完成,就告诉我们 -1
呐。
猫娘的贪心攻略喵~
这道题的目标是最小化天数,一看到“最小化”,我们通常会想到贪心或者动态规划,对吧?
在这里,一个非常自然的想法是:在每一天,我们都应该尽可能多地打败怪物。因为每天都要消耗一位英雄(也就是一天的时间),那让这一天变得“价值最大”,就是打败尽可能多的怪物!这个贪心策略听起来很不错,我们就顺着这个思路往下想,喵~
假设我们今天从第 i
只怪物开始打,我们想一口气打败 k
只怪物(也就是从第 i
只到第 i+k-1
只)。要做到这一点,需要满足什么条件呢?
- 力量要足够:我们派出的英雄,他的力量必须比这
k
只怪物里 最强 的那一只还要强(或者相等)。设这k
只怪物中的最大力量是max_p
,那么英雄的力量p
必须p >= max_p
。 - 耐力要足够:这位英雄的耐力
s
必须足以让他打败k
只怪物,也就是s >= k
。
所以,对于一个怪物区间,我们需要找到一位英雄,同时满足这两个条件。
英雄们的潜力激发!
为了快速找到满足条件的英雄,我们得先对英雄数据做一点预处理,让他们随时准备好出战,喵!
同力量英雄的最优选择:如果好几个英雄力量值相同,我们肯定会选耐力最高的那一个,对吧?用一个
std::map<int, int>
可以很方便地存下力量 -> 最大耐力
的映射,还能自动按力量排序呢!力量与耐力的权衡:如果我们选了一个力量为
P
的英雄,那么任何力量比P
更大的英雄其实也都能用。我们真正关心的是:对于一个最低力量要求max_p
,我们能找到的、所有力量不低于max_p
的英雄中,耐力最高 的是多少?
为了解决这个问题,我们可以做一个小小的处理。先把英雄们按力量从小到大排序,然后从后往前遍历,更新每个英雄的耐力值,让 heroes[i].endurance = max(heroes[i].endurance, heroes[i+1].endurance)
。这样处理完后,heroes[i]
的耐力值就代表了 所有力量不小于 heroes[i].power
的英雄中的最大耐力!是不是很神奇?这就叫后缀最大值,喵~
经过这样的预处理,对于任何一个力量要求 max_p
,我们只需要用二分查找(比如 std::lower_bound
)找到第一个力量不小于 max_p
的英雄,他所记录的耐力值就是我们能得到的最大耐力了!
模拟每一天!
准备工作完成,现在可以开始我们每天的屠龙之旅了!
我们用一个指针 i
记录当前打到哪只怪物了。
- 开始新的一天,天数
days
加一。 - 从第
i
只怪物开始,我们尝试组建一个当天要打的“怪物包”。设这个包里有k
只怪物(从i
到i+k-1
)。 - 计算这个包里怪物的最大力量
max_p_chunk
。 - 用我们预处理好的数据,快速查询:要打败力量为
max_p_chunk
的怪物,我们能得到的最大耐力s_avail
是多少。 - 比较一下,如果
s_avail >= k
,说明我们有能力打败这k
只怪物!太棒了!我们贪心地尝试把下一只怪物(第i+k
只)也加入这个包,继续循环。 - 如果
s_avail < k
,说明我们的英雄耐力不够了,打不了这么多。那么今天最多只能打k-1
只怪物。这一天的工作就到此为止。 - 更新
i
,让它指向下一天要打的第一只怪物,然后开始新的一天。
特殊情况:不可能完成的任务
如果在某一天,我们连一只怪物都打不败(比如,尝试打第 i
只怪物,但找不到任何一个英雄比它强),那就说明任务不可能完成了,直接输出 -1
吧!在我们的循环里,这表现为内层循环一次都没能成功推进,i
的值没有变化。
代码魔法,变身!
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
int m;
std::cin >> m;
// 使用 map 来存储每个力量值对应的最大耐力,顺便按力量排序
std::map<int, int> power_to_max_s;
for (int i = 0; i < m; ++i) {
int p, s;
std::cin >> p >> s;
power_to_max_s[p] = std::max(power_to_max_s[p], s);
}
// 将 map 转换为 vector,方便后续处理
std::vector<std::pair<int, int>> heroes;
if (!power_to_max_s.empty()) {
heroes.reserve(power_to_max_s.size());
for (auto const& [p, s] : power_to_max_s) {
heroes.push_back({p, s});
}
// 关键的预处理:计算耐力的后缀最大值
// heroes[i].second 将表示力量 >= heroes[i].first 的所有英雄中的最大耐力
for (int i = heroes.size() - 2; i >= 0; --i) {
heroes[i].second = std::max(heroes[i].second, heroes[i + 1].second);
}
}
// 一个 lambda 函数,用于快速查询:对于给定的最低力量要求,能得到的最大耐力是多少
auto get_max_s = [&](int power) {
if (heroes.empty()) {
return 0; // 没有英雄,啥也干不了
}
// 使用二分查找找到第一个力量 >= power 的英雄
auto it = std::lower_bound(heroes.begin(), heroes.end(), std::make_pair(power, 0));
if (it == heroes.end()) {
return 0; // 没有英雄的力量足够大
}
// 返回这个英雄(以及所有比他更强的英雄)能提供的最大耐力
return it->second;
};
int days = 0;
int i = 0; // i 是当前待处理的第一个怪物的索引
while (i < n) {
days++;
int start_i = i; // 记录今天开始时打的怪物索引
int max_p_chunk = 0; // 记录今天尝试打的这群怪物中的最大力量
// 贪心:尝试在今天尽可能多地打怪
while (i < n) {
max_p_chunk = std::max(max_p_chunk, a[i]); // 更新怪物群的最大力量
int k = i - start_i + 1; // 尝试打 k 只怪物
int s_avail = get_max_s(max_p_chunk); // 查询打败这群怪物所需的最大可用耐力
// 如果最强的英雄耐力都不够打 k 只,那今天就只能打 k-1 只了
if (s_avail < k) {
break;
}
// 如果耐力足够,就尝试把下一只怪物也加进来
i++;
}
// 如果 i 没有前进,说明连第一只怪物都打不过,任务失败
if (i == start_i) {
std::cout << -1 << "\n";
return;
}
}
std::cout << days << "\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+m) log m) 的说。
- 首先,英雄预处理部分,用
map
插入是O(m log m)
。然后计算后缀最大值是O(m)
。 - 接着是模拟打怪的过程。外层
while
循环最多执行n
次(最坏情况每天打一个)。内层while
循环的指针i
在整个solve
函数中只会从0
遍历到n
一次。在内层循环中,每次我们都会调用get_max_s
,它内部是一个二分查找,复杂度是O(log m)
。所以这部分总的复杂度是O(n log m)
。 - 两部分加起来,总时间复杂度就是
O(m log m + n log m)
呐。
- 首先,英雄预处理部分,用
空间复杂度: O(n + m) 的说。
- 我们需要存储
n
只怪物的力量,这是O(n)
。 - 还需要存储最多
m
位英雄的信息(用map
和vector
),这是O(m)
。 - 所以总的空间开销是
O(n + m)
。
- 我们需要存储
猫娘的小课堂时间~
这道题真是一个很好的例子,告诉我们如何通过 贪心 + 预处理 来解决问题,喵~
- 核心思想:贪心。每天的目标都是最大化收益(打败最多的怪物)。
- 关键技巧:预处理英雄数据,特别是 后缀最大值 的思想,它把一个复杂的“查询最佳英雄”问题,变成了一个简单的二分查找问题。这大大优化了效率!
- 数据结构:
std::map
在这里起到了去重、排序和初步聚合的作用。std::vector
配合std::lower_bound
则是实现高效查询的利器。 - 注意事项:一定要想清楚边界情况和无解的情况!比如,如果没有任何英雄能打败当前怪物组里最弱的一只,那游戏就进行不下去了,要及时判断并输出
-1
哦。
只要思路清晰,一步步把问题拆解开,再难的题目也能解决的,主人要对自己有信心,加油喵!