蓝桥杯 第 2 场算法双周赛 第4题 通关【算法赛】c++ 优先队列 + 小根堆 详解注释版

1 篇文章 0 订阅
1 篇文章 0 订阅

 题目

通关【算法赛】icon-default.png?t=N7T8https://www.lanqiao.cn/problems/5889/learning/?contest_id=145

问题描述

小蓝最近迷上了一款电玩游戏“蓝桥争霸”。这款游戏由很多关卡和副本组成,每一关可以抽象为一个节点,整个游戏的关卡可以抽象为一棵树形图,每一关会有一道算法题,只有当经验值不低于第 i 关的要求 ki​ 时,小蓝才能挑战成功通过此关,并且获得 si​ 的经验值,每关的经验值只能获得一次。每个关卡(除了 11 号点),都会有一个前置关卡,只有通过了前置关卡,才能挑战后续关卡。

小蓝初始在 11 号点,也就是游戏开局初始点,同时具有一个初始经验值 P,他可以任意规划挑战顺序,他想问你最多能够挑战成功多少道题。

小蓝会告诉你关卡的所有信息,以及他的初始经验值,你需要回答他最多能够挑战成功多少关卡。

输入格式

第一行输入两个整数 n,P,表示关卡的数量以及小蓝的初始经验值。

接下来 n 行,每行输入三个整数 fi​,si​,ki​,fi​ 表示每一关的前置关卡( f1​ 一定为 00 ),si​ 表示经验值,ki​ 表示挑战成功最少需要的经验值。

输出格式

一个整数,表示在最优的规划下,最多能挑战成功的关卡数量。

样例输入

4 5
0 3 5
1 2 8
1 3 9
3 1 15

样例输出

3

说明

游戏地图如下:

关卡描述

小蓝初始点在 11 号关卡,初始经验为 55。每个关卡具有挑战前提:11 号关卡可以直接挑战,如果要挑战 22 号关卡,必须通过 11 号关卡,3,43,4号关卡类似。

小蓝的一种挑战顺序如下:

  1. 由于初始经验为 55,满足 11 号关卡要求,所以可以直接挑战成功 11 号关卡,获得 33 经验值,此时经验值为 88,并且获得挑战 2,32,3 号关卡的机会。

  2. 此时经验为 88,满足 22 号关卡要求,但是不满足 33 号要求,所以可以直接挑战成功 22 号关卡,获得 22 经验值,此时经验值为 1010。

  3. 此时经验为 1010,满足 33 号关卡要求,所以对 33 号关卡挑战成功,获得 33 经验值,此时经验值为 1313,并且获得挑战 44 号关卡的机会。

  4. 此时经验为 1313,小于 44 号关卡要求,所以无法成功挑战 44 号关卡,游戏无法继续。

评测数据范围

f1​=0<fi​≤n≤105,0≤P,si​,ki​≤109。

数据保证输入为一棵树,并且根节点为 11。

运行限制

语言最大运行时间最大运行内存
C++1s256M
C1s256M
Java2s256M
Python33s256M
PyPy33s256M

思路和解题方法

首先,定义了一些全局变量和类型别名:

  • N 定义为 1e5+100,表示任务数量的上限。
  • Pair 是一个包含两个整数的类型别名,用于存储任务的优先级和任务编号。
  • G 是一个邻接表,用于存储任务关系图,每个任务的子任务列表存储在 G 中。
  • S 数组用于存储每个任务的耗能值。
  • K 数组用于存储每个任务的优先级。
  • n 表示任务数量。
  • P 表示初始能量值。
  • cntvis 是辅助变量。

接下来是 sol 函数,用于解决任务调度问题。函数的主要逻辑如下:

  1. 读取任务数量 n 和初始能量值 P
  2. 使用循环读取每个任务的父任务编号 f、耗能值 S[i] 和优先级 K[i],并将每个任务添加到对应父任务的子任务列表中。
  3. 创建一个最小堆(优先队列) q,用于存储任务的优先级和任务编号,按照优先级从小到大排序。
  4. 将初始任务(编号为0)的优先级和编号加入队列。
  5. 初始化完成任务的计数器 ccnt 为 -1,因为初始任务不计入完成数量。
  6. 进入循环,当队列不为空时进行以下操作:
    • 检查当前能量值是否大于等于队首任务的优先级。
    • 如果满足条件,表示能够执行任务:
      • 完成任务数量 ccnt 加一。
      • 取出队首任务,并将其从队列中弹出。
      • 将任务的能量值加到当前能量值上。
      • 遍历该任务的子任务,将子任务的优先级和编号加入队列。
    • 如果当前能量值不足以执行队首任务,跳出循环。
  7. 输出完成任务的数量 ccnt

        使用了优先队列(最小堆)来实现任务调度。通过不断执行优先级最高的任务,并更新能量值,直到能量值不足以执行下一个任务为止。最终输出完成的任务数量。

复杂度

        时间复杂度:

                O(n^2)

时间复杂度:

  • 读取任务数量和初始能量值的时间复杂度为 O(1)。
  • 循环读取每个任务的父任务编号、耗能值和优先级的时间复杂度为 O(n)。
  • 创建优先队列 q 的时间复杂度为 O(nlogn),其中 n 是任务数量。
  • 循环执行任务的时间复杂度取决于任务的数量和任务之间的关系。在最坏情况下,每个任务都是其他任务的子任务,因此循环执行任务的时间复杂度为 O(n^2)。但是,如果任务之间的关系是稀疏的,循环执行任务的时间复杂度可能会小于 O(n^2)。
  • 输出完成任务数量的时间复杂度为 O(1)。

总结,整个算法的时间复杂度在最坏情况下为 O(n^2),但在实际情况下可能会更好。

        空间复杂度:

                O(n)

空间复杂度:

  • 邻接表 G 的空间复杂度为 O(n),存储了任务之间的关系。
  • 数组 S 和 K 的空间复杂度为 O(n),存储了每个任务的耗能值和优先级。
  • 优先队列 q 的空间复杂度为 O(n),存储了任务的优先级和任务编号。
  • 辅助变量的空间复杂度可以忽略不计。

总结,整个算法的空间复杂度为 O(n),其中 n 是任务数量。

c++ 代码

#include <iostream>
#include <vector>
#include <queue> // 包含优先队列的头文件
using namespace std;

const int N = 1e5 + 100; // 定义任务数量的上限

typedef pair<int, int> Pair; // 定义一个包含两个整数的类型别名
vector<int> G[N]; // 邻接表,存储任务之间的关系
int S[N], K[N]; // 数组,存储每个任务的耗能值和优先级
int n, P, cnt, vis[N]; // 全局变量,表示任务数量、初始能量值、辅助变量

void sol() {
    cin >> n >> P; // 读取任务数量和初始能量值

    for (int i = 1; i <= n; i++) {
        int f;
        cin >> f >> S[i] >> K[i]; // 读取父任务编号、耗能值和优先级
        G[f].push_back(i); // 将当前任务加入对应父任务的子任务列表中
    }

    priority_queue<Pair, vector<Pair>, greater<Pair>> q; // 创建一个最小堆,用于存储任务的优先级和任务编号
    q.push({K[0], 0}); // 将初始任务加入队列中

    int ccnt = -1; // 完成任务的计数器,初始值为 -1,因为初始任务不计入完成数量

    while (!q.empty()) { // 当队列不为空时,循环执行任务
        auto [k, u] = q.top(); // 取出队首任务的优先级和编号
        q.pop(); // 将队首任务从队列中弹出

        if (P >= k) { // 如果当前能量值可以执行队首任务
            P += S[u]; // 将任务的能量值加到当前能量值上
            ccnt++; // 完成任务数量加一

            for (auto v : G[u]) { // 遍历该任务的子任务
                q.push({K[v], v}); // 将子任务的优先级和编号加入队列
            }
        } else { // 如果当前能量值不足以执行队首任务
            break; // 跳出循环
        }
    }

    cout << ccnt << endl; // 输出完成任务的数量
}

int main() {
    sol(); // 调用 sol 函数解决问题
    exit(0); // 终止程序
}

相关知识 和推荐视频

1. pair:  Pair 基础

  • pair 是 C++ 标准库中的一个模板类,用于存储两个值的有序对。它定义在 <utility> 头文件中。
  • pair 类模板接受两个类型参数,分别表示第一个值的类型和第二个值的类型。例如,pair<int, double> 表示一个包含一个整数和一个浮点数的有序对。
  • pair 类模板的实例可以通过花括号 {} 初始化,也可以通过构造函数进行初始化。可以使用 firstsecond 成员变量来访问有序对中的第一个值和第二个值。

2. 优先队列   小顶堆  堆 的介绍 建议选着来看,看看大小堆是啥有啥用,就行了。 

建议跳看

 priority_queue<Pair, vector<Pair>, greater<Pair>> q;
  • priority_queue 是 C++ 标准库中的一个模板类,用于实现优先队列(堆)。它定义在 <queue> 头文件中。
  • priority_queue 类模板接受三个类型参数,分别表示存储在队列中的元素类型、底层容器类型和比较函数对象类型。例如,priority_queue<int, vector<int>, greater<int>> 表示一个存储整数的优先队列,使用 vector 作为底层容器,并按照从小到大的顺序进行排序。
  • 在代码中,Pair 是一个包含两个整数的类型别名,vector<Pair> 表示使用 vector 作为底层容器来存储 Pair 类型的元素,greater<Pair> 表示使用 greater 函数对象来定义元素之间的比较关系。
  • greater<Pair> 是一个函数对象,它定义了对 Pair 类型的元素进行比较的方式。在这里,greater<Pair> 使用 operator() 函数重载来比较两个 Pair 类型的元素,根据 Pair 中第一个值的大小进行比较。因此,priority_queue<Pair, vector<Pair>, greater<Pair>> 创建的优先队列会按照 Pair 中第一个值的从小到大的顺序进行排序。

3.扩展

大顶堆

大顶堆可以通过使用 less 函数对象来定义元素之间的比较关系。

在 C++ 中,priority_queue 默认使用 less 函数对象来实现大顶堆。

4.再解释一下内容

while (!q.empty()) {
        if (P >= q.top().first) { // 当当前能量值大于等于队首任务的优先级时,执行任务
            ccnt ++; // 完成任务数量加一
            Pair tmp = q.top(); // 取出队首任务
            q.pop(); // 弹出队首任务
            P += S[tmp.second]; // 将任务的能量值加到当前能量值上
            for (int v : G[tmp.second]) { // 遍历该任务的子任务
                q.push({K[v], v}); // 将子任务的优先级和编号加入队列
            }
        } else {
            break; // 当前能量值不足以执行队首任务时,跳出循环
        }
    }
  1. while (!q.empty()):当任务队列不为空时,执行循环体内的代码。

  2. if (P >= q.top().first):判断当前能量值 P 是否大于等于队首任务的优先级 q.top().first。如果是,则执行任务;否则,跳出循环。

  3. ccnt++:完成任务数量加一。

  4. Pair tmp = q.top():取出队首任务,并将其存储在临时变量 tmp 中。

  5. q.pop():弹出队首任务,将其移出任务队列。

  6. P += S[tmp.second]:将任务的能量值 S[tmp.second] 加到当前能量值 P 上,表示执行任务消耗了一定的能量。

  7. for (int v : G[tmp.second]):遍历该任务的子任务,使用范围-based for 循环,将每个子任务的编号存储在变量 v 中。

  8. q.push({K[v], v}):将子任务的优先级 K[v] 和编号 v 加入任务队列 q 中,表示将子任务加入待执行的任务队列中。

  9. else:当当前能量值不足以执行队首任务时,跳出循环。

觉得有用的话可以点点赞,支持一下。

如果愿意的话关注一下。会对你有更多的帮助。

每天都会不定时更新哦  >人<  。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值