Governing sand - 题解
标签与难度
标签: 贪心, 排序, 枚举, 桶排序思想, 前缀和思想 难度: 1700
题目大意喵~
主人你好呀~ 这道题是说,有一个被风沙困扰的Wowo村,村前有一片森林可以防沙。但这片森林要想起作用,必须满足一个奇特的条件:森林里最高的树的数量必须严格大于森林里所有树总数的一半,喵~
我们可以花钱砍掉一些树来满足这个条件。现在有 n 种树,每种树都有自己的高度 H、砍伐单价 C 和数量 P。我们的任务就是,用最少的钱来改造森林,让它达到防沙的要求。
输入会给出 n,以及接下来 n 行,每行是 H_i, C_i, P_i。我们要输出一个数字,就是最少的总花费,呐。
解题思路分析
这道题的目标是求最小花费,通常会和动态规划或者贪心有关,喵~ 让我们先来分析一下这个奇怪的防沙条件。
假设我们改造完森林后,最高的树的高度是 H_max,这种高度的树有 N_tallest 棵,而森林里剩下的其他树(高度小于 H_max)一共有 N_other 棵。那么,此时森林的总树木数量就是 N_total = N_tallest + N_other。
题目要求是 N_tallest > N_total / 2。我们来把这个式子变个形,看看能不能发现什么小秘密~ N_tallest > (N_tallest + N_other) / 2 两边都乘以2,得到: 2 * N_tallest > N_tallest + N_other 再把一个 N_tallest 移到右边: N_tallest > N_other
哇!原来这个复杂的条件,本质上就是说最高树的数量必须比其他所有树加起来还要多!只要满足这个,就大功告成啦,的说。
那么,最终森林里最高的树是哪一种呢?我们并不知道。但是,最终的最高高度 H_max 一定是原来存在的某一种树的高度 H_i。为什么呢?因为我们只能砍树,不能把树变高。如果我们选择一个不存在的高度作为 H_max,那最高的树就是空的,数量为0,肯定不行呀。
所以,一个很自然的想法就诞生了:我们可以枚举最终保留的最高树的高度 H_target 是多少。对于每一种可能的 H_target,我们都计算出达成目标的最小花费,然后取所有这些花费中的最小值,就是最终答案啦!
好,现在我们来固定一个目标高度 H_target,看看需要做什么:
强制砍伐:所有高度大于
H_target的树都必须被砍掉,不然H_target就不是最高高度了。这部分的开销是固定的,我们必须支付。确定主要群体:所有高度等于
H_target的树,就成了我们新的“最高树天团”,我们把它们的总数记为N_tallest。确定其他群体:所有高度小于
H_target的树,就是“其他树”,总数记为N_other。判断与调整:现在我们来检查
N_tallest > N_other是否成立。- 如果已经成立了,太棒了!我们只需要支付第一步的强制砍伐费用。
- 如果不成立,即
N_tallest <= N_other,我们就必须继续砍树。为了尽快满足N_tallest > N_other,我们应该从N_other这个群体里砍树。因为如果砍N_tallest里的树,会让N_tallest变少,离目标更远了,不划算喵~ - 我们需要砍掉多少棵呢?为了让
N_other变得比N_tallest小,最少要让N_other减小到N_tallest - 1。所以,需要砍掉的树的数量是N_other - (N_tallest - 1)棵。 - 砍哪些呢?当然是砍
N_other群体里最便宜的那些树啦!这是一个非常经典的贪心策略。
把上面的思路整理一下,一个高效的算法就出现啦:
- 首先,把所有种类的树按照高度从高到低排序。这样方便我们从最高的树开始枚举。
- 我们用一个循环,从最高的树种开始,依次考虑每一种高度作为
H_target。 - 为了快速找到需要砍的
k棵最便宜的树,我们可以用一个频率数组(桶)cost_counts来记录当前可砍的“其他树”里,各种费用的树分别有多少棵。因为费用C的范围很小(1到200),用桶来统计再合适不过了! - 我们从高到低遍历所有不同的高度。假设当前处理的高度是
H_curr:- 所有比
H_curr更高的树,都已经被我们“处理”过了,它们要么被强制砍伐,要么在之前的步骤里被当成过H_target。我们可以维护一个变量cost_of_mandatory_cuts来记录砍掉所有高于H_curr的树的总费用。 - 所有高度等于
H_curr的树,组成了我们的N_tallest。 - 所有高度小于
H_curr的树,组成了我们的N_other。它们的费用分布就存在我们的cost_counts桶里。 - 我们计算需要从
N_other中砍掉k = N_other - (N_tallest - 1)棵树。 - 然后利用
cost_counts桶,从费用1开始,贪心地砍掉k棵最便宜的树,计算出这部分的optional_cut_cost。 - 当前
H_curr作为最高高度的总费用就是cost_of_mandatory_cuts + optional_cut_cost。用它来更新我们的全局最小答案。 - 处理完
H_curr后,我们要把它也加入到“必须砍掉”的行列中,为下一个更低的高度做准备。也就是把砍掉H_curr这批树的费用加到cost_of_mandatory_cuts里,并更新cost_counts桶。
- 所有比
通过这样一次遍历,我们就能找到全局最优解了,喵~ 这个方法把枚举、排序和贪心完美地结合在了一起!
代码实现
这是我根据上面的思路,精心重构的一份代码哦~ 希望能帮到你,喵~
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
// 使用 long long 防止费用和数量相乘后溢出
using int64 = long long;
// 定义一个结构体来存放每种树的信息,方便管理
struct TreeInfo {
int64 h, c, p;
};
// 排序时使用,按高度从高到低排序
bool compareTrees(const TreeInfo& a, const TreeInfo& b) {
return a.h > b.h;
}
void solve() {
int n;
if (!(std::cin >> n)) {
return;
}
std::vector<TreeInfo> trees(n);
int64 total_tree_count = 0;
// 费用的范围是1-200,所以开201大小的桶就够啦
std::vector<int64> cost_counts(201, 0);
for (int i = 0; i < n; ++i) {
std::cin >> trees[i].h >> trees[i].c >> trees[i].p;
total_tree_count += trees[i].p;
cost_counts[trees[i].c] += trees[i].p;
}
// 按高度从高到低排序
std::sort(trees.begin(), trees.end(), compareTrees);
int64 min_total_cost = -1; // 初始化为-1,或者一个非常大的数
int64 cost_of_mandatory_cuts = 0; // 记录强制砍伐的费用
int i = 0;
while (i < n) {
// --- Step 1: 将同高度的树作为一组进行处理 ---
int j = i;
while (j < n && trees[j].h == trees[i].h) {
j++;
}
// 现在 [i, j-1] 区间内的树高度都相同
int64 num_as_tallest = 0; // 当前高度的树的总数
int64 cost_of_this_block = 0; // 当前高度这批树的总价值
for (int k = i; k < j; ++k) {
num_as_tallest += trees[k].p;
cost_of_this_block += trees[k].p * trees[k].c;
// 从桶中移除当前高度的树,因为它们不是“其他树”
cost_counts[trees[k].c] -= trees[k].p;
}
// --- Step 2: 计算需要额外砍伐的树木数量 ---
int64 num_other = total_tree_count - num_as_tallest;
int64 num_to_cut = 0;
if (num_as_tallest <= num_other) {
num_to_cut = num_other - (num_as_tallest - 1);
}
// --- Step 3: 贪心计算额外砍伐的费用 ---
int64 optional_cut_cost = 0;
int64 remaining_to_cut = num_to_cut;
if (remaining_to_cut > 0) {
// 从费用最低的桶开始,贪心砍树
for (int cost = 1; cost <= 200; ++cost) {
if (cost_counts[cost] > 0) {
int64 can_cut = std::min(remaining_to_cut, cost_counts[cost]);
optional_cut_cost += can_cut * cost;
remaining_to_cut -= can_cut;
if (remaining_to_cut == 0) {
break;
}
}
}
}
// --- Step 4: 更新全局最小费用 ---
int64 current_total_cost = cost_of_mandatory_cuts + optional_cut_cost;
if (min_total_cost == -1 || current_total_cost < min_total_cost) {
min_total_cost = current_total_cost;
}
// --- Step 5: 准备下一轮迭代 ---
// 当前这批树在下一轮中将成为“高于目标高度”的树,所以计入强制砍伐成本
cost_of_mandatory_cuts += cost_of_this_block;
// 更新森林中剩余的树木总数
total_tree_count -= num_as_tallest;
// 跳到下一个不同高度的树
i = j;
}
std::cout << min_total_cost << std::endl;
}
int main() {
// 提高cin/cout效率
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// 题目是多组输入
while (std::cin.peek() != EOF && std::cin.peek() != '\n') {
solve();
}
return 0;
}复杂度分析
时间复杂度:
- 首先,我们需要对
N种树按高度进行排序,这部分的时间复杂度是 ,呐。 - 接着,我们遍历排序后的树种。虽然是
while循环,但每个树种只会被访问一次。在循环内部,最耗时的操作是计算optional_cut_cost,这需要遍历费用桶,最多遍历C_{max}(这里是200)次。 - 所以,总的时间复杂度是排序加上遍历,即 。因为
C_{max}是个不大的常数,所以复杂度近似于 。
- 首先,我们需要对
空间复杂度:
- 我们需要一个
vector来存储N种树的信息,空间是 。 - 我们还需要一个频率数组(桶)来统计不同费用的树的数量,其大小为
C_{max}+ 1,所以是 。 - 合计起来,总的空间复杂度就是 ,喵~
- 我们需要一个
知识点总结
这道题真是一次愉快的思维体操呢!我们主要用到了以下几个知识点:
- 问题转化: 将复杂的
N_tallest > N_total / 2条件转化为更直观的N_tallest > N_other,是解题的关键第一步。 - 枚举思想: 当无法直接确定最优解的某个属性时(比如本题的最终最高高度),可以尝试枚举所有可能性,对每种可能性计算一个结果,最后取最优。
- 贪心算法: 在需要做出系列选择以达到最优时,如果每一步都采取当前看起来最好的选择,最终能得到全局最优解,这就是贪心。本题中“砍掉最便宜的树”就是典型的贪心策略。
- 排序: 排序是很多算法的预处理步骤。本题通过按高度排序,使得我们可以有序地、不重不漏地枚举所有情况,并高效地维护状态。
- 桶/频率数组: 对于值域范围不大的数据(如本题的费用
C),使用桶来计数是一种非常高效的技巧,可以让我们在 的时间内访问特定值的数据,或者像本题一样快速找到最小/最大的元素。
希望这篇题解能对你有所帮助,如果还有不明白的地方,随时可以再来问我哦,喵~