瑞瑞的能源网络

【题目来源】

瑞学堂:瑞瑞的能源网络

【题目描述】

瑞瑞正在为一座未来城市设计能源分配网络。城市可以看作一个包含 \(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

【核心思想】

  1. 问题分析:给定一棵 \(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\) 时,最多能满足的部门数量。这是一个反悔贪心问题,关键在于按门槛排序后,用堆维护已选任务,实现"用更优任务替换已选劣任务"的贪心策略。

  2. 算法选择

    • DFS 预处理:计算从节点 \(1\) 到每个节点的去程损耗 \(go_i\) 和回程损耗 \(back_i\)
    • 任务参数化:对每个部门计算 require = go_i + max(back_i, d_i)(最低能量门槛)和 cost = go_i + back_i(实际消耗)
    • 反悔贪心 + 大根堆:按 require 升序遍历任务,能执行则执行并入堆;不能执行则尝试用当前任务替换堆中 cost 最大的已选任务(若替换后更优)
  3. 关键步骤

    • 建图与 DFS:读入 \(n, m, E\)\(n-1\) 条边,用邻接表存储双向不对称边,从节点 \(1\) DFS 计算所有节点的 \(go\)\(back\)
    • 生成任务列表:对每个部门,若 require <= E 则加入 tasks(过滤不可行任务)
    • 排序:按 require 升序排序任务
    • 反悔贪心遍历
      • cur >= t.requirecur -= t.cost,将 t.cost 入堆
      • 否则若堆非空且 pq.top() > t.cost:若 cur + pq.top() >= t.require,则弹出堆顶,cur = cur + pq.top() - t.cost,将 t.cost 入堆
    • 输出堆的大小
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m \log m)\),DFS \(O(n)\),排序 \(O(m \log m)\),堆操作 \(O(m \log m)\)
    • 空间复杂度:\(O(n + m)\),邻接表、任务列表、堆
  5. 反悔贪心的核心思想

    • 门槛优先的贪心基线:按 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
posted @ 2026-08-06 17:01  团爸讲算法  阅读(6)  评论(0)    收藏  举报