瑞瑞的能源网络
【题目来源】
瑞学堂:瑞瑞的能源网络
【题目描述】
瑞瑞正在为一座未来城市设计能源分配网络。城市可以看作一个包含 \(n\) 个节点的树状结构,节点编号为 \(1\) 到 \(n\),其中 \(1\) 号节点是总能源站。城市中的每条边都代表一条能源管道,管道会因材质和长度不同而产生不同的能量损耗。具体来说,对于树上的每条边 \((u,v)\),如果能量从 \(u\) 传输到 \(v\),都会有一个对应的损耗值。损耗值是双向不对称的,即从 \(u\) 到 \(v\) 的损耗为 \(w_{u,v}\),而从 \(v\) 到 \(u\) 的损耗为 \(w_{v,u}\)。
城市中的 \(m\) 个关键部门分布在一些节点上,每个部门都有一个固定的能量需求 \(d_i\)。瑞瑞希望从总能源站(节点 \(1\))出发,为一些部门输送能量。由于输送技术限制,他每次只能选择一个部门,携带一定能量从节点 \(1\) 沿简单路径前往该部门所在节点,满足其需求后沿原路返回节点 \(1\),这称为一次运输。各次运输之间相互独立,但总能量 \(E\) 是所有运输消耗的总和。在一次运输中,当他到达目标部门所在节点时,所携带的剩余能量必须至少等于该部门的需求 \(d_i\),此时才能成功满足需求(注意:能量不会被需求消耗,仅作为阈值判断)。每次运输的能量消耗等于路径上各边损耗之和,即去程消耗从 \(1\) 到目标节点方向的总损耗,回程消耗从目标节点回到 \(1\) 方向的总损耗。
运输过程中,每次运输结束后,未消耗的能量会自动回收至总能量池,可供后续运输使用。因此,在初始拥有总能量 \(E\) 的条件下,瑞瑞可以任意安排运输顺序。对于第 \(i\) 个部门所在的节点,设从 \(1\) 到该节点的去程损耗为 \(go_i\),回程损耗为 \(back_i\)。一次运输可行当且仅当在执行该运输时,剩余能量 \(≥go_i+max(back_i,d_i)\),且该次运输累计消耗 \(go_i+back_i\)。
请你帮助瑞瑞计算,在总消耗不超过 \(E\) 的前提下,他最多能成功满足多少个部门的需求。
【输入】
第一行包含三个整数 \(n,m,E\),分别表示城市节点数、关键部门数以及初始能量值。
接下来 \(n−1\) 行,每行包含四个整数 \(u,v,w_1,w_2\),表示节点 \(u\) 和 \(v\) 之间有一条管道,从 \(u\) 到 \(v\) 的损耗为 \(w_1\),从 \(v\) 到 \(u\) 的损耗为 \(w_2\)。
接下来 \(m\) 行,每行包含两个整数 \(p_i,d_i\),表示第 \(i\) 个关键部门所在的节点编号以及其能量需求。部门可能位于同一个节点上。
【输出】
输出一个整数,表示在初始能量为 \(E\) 且必须返回节点 \(1\) 的前提下,最多能满足的部门数量。
【输入样例】
3 2 14
1 2 1 1
1 3 2 6
2 8
3 10
【输出样例】
2
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的树(\(1\) 为根),每条边有双向不对称损耗 \(w_{u,v}\) 和 \(w_{v,u}\)。\(m\) 个部门分布在节点上,各有需求 \(d_i\)。每次运输从 \(1\) 出发到某部门再返回,消耗往返损耗之和。运输可行当且仅当执行时剩余能量 \(\geq go_i + \max(back_i, d_i)\)(\(go_i\) 为去程损耗,\(back_i\) 为回程损耗)。求总消耗不超过初始能量 \(E\) 时,最多能满足的部门数量。这是一个反悔贪心问题,关键在于按门槛排序后,用堆维护已选任务,实现"用更优任务替换已选劣任务"的贪心策略。
-
算法选择:
- DFS 预处理:计算从节点 \(1\) 到每个节点的去程损耗 \(go_i\) 和回程损耗 \(back_i\)
- 任务参数化:对每个部门计算
require = go_i + max(back_i, d_i)(最低能量门槛)和cost = go_i + back_i(实际消耗) - 反悔贪心 + 大根堆:按
require升序遍历任务,能执行则执行并入堆;不能执行则尝试用当前任务替换堆中cost最大的已选任务(若替换后更优)
-
关键步骤:
- 建图与 DFS:读入 \(n, m, E\) 和 \(n-1\) 条边,用邻接表存储双向不对称边,从节点 \(1\) DFS 计算所有节点的 \(go\) 和 \(back\)
- 生成任务列表:对每个部门,若
require <= E则加入tasks(过滤不可行任务) - 排序:按
require升序排序任务 - 反悔贪心遍历:
- 若
cur >= t.require:cur -= t.cost,将t.cost入堆 - 否则若堆非空且
pq.top() > t.cost:若cur + pq.top() >= t.require,则弹出堆顶,cur = cur + pq.top() - t.cost,将t.cost入堆
- 若
- 输出堆的大小
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m \log m)\),DFS \(O(n)\),排序 \(O(m \log m)\),堆操作 \(O(m \log m)\)
- 空间复杂度:\(O(n + m)\),邻接表、任务列表、堆
-
反悔贪心的核心思想:
- 门槛优先的贪心基线:按
require升序处理,保证当前能量能执行的任务一定被考虑,这是"尽可能多完成任务"的基础 - 实际消耗的资源竞争:总能量有限,完成任务数最大化等价于在门槛约束下最小化总消耗
- 堆维护替换候选:大根堆存储已选任务的
cost,堆顶是消耗最大的任务。当遇到新任务无法执行时,若其cost小于堆顶,替换可减少总消耗,可能释放足够能量执行更多任务 - 延迟替换的正确性:不立即替换所有可能情况,而是在遍历过程中动态调整,保证最终堆中任务是门槛约束下的最小消耗组合
- 适用于"在满足门槛约束的前提下,用有限资源最大化任务数量"的问题,核心在于将任务量化为(门槛,消耗)二元组,通过排序和堆实现动态最优替换
- 门槛优先的贪心基线:按
【算法标签】
反悔贪心
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int定义为long long,避免能量计算溢出
const int N = 100005; // 定义数组最大容量为100005
int n, m, E; // n为节点数,m为部门数,E为初始总能量
struct Edge
{
int v, go, back; // v为邻接节点,go为从u到v的损耗,back为从v到u的损耗
};
vector<Edge> adj[N]; // adj[u]存储节点u的所有邻接边(双向不对称损耗)
int go[N], back[N]; // go[i]为从节点1到i的去程总损耗,back[i]为从i回到1的回程总损耗
// 定义任务结构体:每个部门运输所需的能量门槛和实际消耗
struct Task
{
int require, cost; // require为执行该运输所需的最低能量门槛,cost为该运输的实际能量消耗
};
vector<Task> tasks; // 存储所有可行的部门运输任务
priority_queue<int> pq; // 大根堆,存储已选任务的实际消耗,用于后续替换优化
// 深度优先搜索:计算从节点1到每个节点的去程和回程总损耗
void dfs(int u, int fa)
{
for (auto x : adj[u]) // 遍历u的所有邻接边
{
int v = x.v, w1 = x.go, w2 = x.back; // v为邻接节点,w1为u->v损耗,w2为v->u损耗
if (v == fa) // 跳过父节点,避免回溯
continue;
go[v] = go[u] + w1; // 累加去程损耗:从1到v = 从1到u + u到v
back[v] = back[u] + w2; // 累加回程损耗:从v到1 = 从u到1 + v到u
dfs(v, u); // 递归遍历子节点
}
}
// 自定义排序:按运输所需能量门槛升序排列(贪心:优先满足门槛低的部门)
bool cmp(Task x, Task y)
{
return x.require < y.require;
}
signed main() // 使用signed main配合#define int long long
{
cin >> n >> m >> E; // 读入节点数、部门数、初始能量
for (int i = 1; i < n; i++) // 读入n-1条边的信息
{
int u, v, w1, w2;
cin >> u >> v >> w1 >> w2;
adj[u].push_back({v, w1, w2}); // u->v损耗w1,v->u损耗w2
adj[v].push_back({u, w2, w1}); // v->u损耗w2,u->v损耗w1
}
go[1] = back[1] = 0; // 节点1为起点,去程和回程损耗均为0
dfs(1, 0); // 从节点1开始DFS,计算所有节点的go和back
// 处理每个部门,计算其运输任务参数
for (int i = 1; i <= m; i++)
{
int p, d; // p为部门所在节点,d为部门能量需求
cin >> p >> d;
int go_cost = go[p]; // 去程损耗
int back_cost = back[p]; // 回程损耗
// 执行该运输的最低能量门槛:去程损耗 + max(回程损耗, 部门需求)
// 因为到达时剩余能量必须≥需求d,且还要能返回,所以需要max(back, d)
int require = go_cost + max(back_cost, (int)d);
// 该运输的实际总消耗:去程损耗 + 回程损耗(能量只是门槛判断,实际消耗是往返总和)
int cost = go_cost + back_cost;
// 如果初始能量E足够满足该部门的最低门槛,则该任务可行
if (require <= E)
tasks.push_back({require, cost}); // 加入可行任务列表
}
sort(tasks.begin(), tasks.end(), cmp); // 按门槛升序排序
// 贪心+堆优化:尽可能多地满足部门需求
int cur = E; // cur记录当前剩余能量
for (auto t : tasks) // 按门槛从低到高遍历每个可行任务
{
if (cur >= t.require) // 如果当前剩余能量足够执行该运输
{
cur -= t.cost; // 扣除该运输的实际消耗
pq.push(t.cost); // 将该任务的消耗加入大根堆
}
// 如果当前能量不足以执行新任务,但可以用更省能量的任务替换已选任务
else if (!pq.empty() && pq.top() > t.cost) // 如果堆顶(已选最大消耗)大于当前任务消耗
{
int tmp = pq.top(); // 取出堆顶(消耗最大的已选任务)
// 如果替换后能量足够执行当前任务
if (cur + tmp >= t.require)
{
pq.pop(); // 移除堆顶任务
cur = cur + tmp - t.cost; // 回收堆顶消耗,扣除当前消耗,更新剩余能量
pq.push(t.cost); // 将当前任务加入堆
}
}
}
cout << pq.size() << endl; // 输出堆的大小,即最多能满足的部门数量
return 0;
}
【运行结果】
3 2 14
1 2 1 1
1 3 2 6
2 8
3 10
2
浙公网安备 33010602011771号