题目
通关【算法赛】https://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号关卡类似。
小蓝的一种挑战顺序如下:
-
由于初始经验为 55,满足 11 号关卡要求,所以可以直接挑战成功 11 号关卡,获得 33 经验值,此时经验值为 88,并且获得挑战 2,32,3 号关卡的机会。
-
此时经验为 88,满足 22 号关卡要求,但是不满足 33 号要求,所以可以直接挑战成功 22 号关卡,获得 22 经验值,此时经验值为 1010。
-
此时经验为 1010,满足 33 号关卡要求,所以对 33 号关卡挑战成功,获得 33 经验值,此时经验值为 1313,并且获得挑战 44 号关卡的机会。
-
此时经验为 1313,小于 44 号关卡要求,所以无法成功挑战 44 号关卡,游戏无法继续。
评测数据范围
f1=0<fi≤n≤105,0≤P,si,ki≤109。
数据保证输入为一棵树,并且根节点为 11。
运行限制
语言 | 最大运行时间 | 最大运行内存 |
---|---|---|
C++ | 1s | 256M |
C | 1s | 256M |
Java | 2s | 256M |
Python3 | 3s | 256M |
PyPy3 | 3s | 256M |
思路和解题方法
首先,定义了一些全局变量和类型别名:
N
定义为 1e5+100,表示任务数量的上限。Pair
是一个包含两个整数的类型别名,用于存储任务的优先级和任务编号。G
是一个邻接表,用于存储任务关系图,每个任务的子任务列表存储在G
中。S
数组用于存储每个任务的耗能值。K
数组用于存储每个任务的优先级。n
表示任务数量。P
表示初始能量值。cnt
、vis
是辅助变量。接下来是
sol
函数,用于解决任务调度问题。函数的主要逻辑如下:
- 读取任务数量
n
和初始能量值P
。- 使用循环读取每个任务的父任务编号
f
、耗能值S[i]
和优先级K[i]
,并将每个任务添加到对应父任务的子任务列表中。- 创建一个最小堆(优先队列)
q
,用于存储任务的优先级和任务编号,按照优先级从小到大排序。- 将初始任务(编号为0)的优先级和编号加入队列。
- 初始化完成任务的计数器
ccnt
为 -1,因为初始任务不计入完成数量。- 进入循环,当队列不为空时进行以下操作:
- 检查当前能量值是否大于等于队首任务的优先级。
- 如果满足条件,表示能够执行任务:
- 完成任务数量
ccnt
加一。- 取出队首任务,并将其从队列中弹出。
- 将任务的能量值加到当前能量值上。
- 遍历该任务的子任务,将子任务的优先级和编号加入队列。
- 如果当前能量值不足以执行队首任务,跳出循环。
- 输出完成任务的数量
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
类模板的实例可以通过花括号{}
初始化,也可以通过构造函数进行初始化。可以使用first
和second
成员变量来访问有序对中的第一个值和第二个值。
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; // 当前能量值不足以执行队首任务时,跳出循环
}
}
while (!q.empty())
:当任务队列不为空时,执行循环体内的代码。
if (P >= q.top().first)
:判断当前能量值P
是否大于等于队首任务的优先级q.top().first
。如果是,则执行任务;否则,跳出循环。
ccnt++
:完成任务数量加一。
Pair tmp = q.top()
:取出队首任务,并将其存储在临时变量tmp
中。
q.pop()
:弹出队首任务,将其移出任务队列。
P += S[tmp.second]
:将任务的能量值S[tmp.second]
加到当前能量值P
上,表示执行任务消耗了一定的能量。
for (int v : G[tmp.second])
:遍历该任务的子任务,使用范围-based for 循环,将每个子任务的编号存储在变量v
中。
q.push({K[v], v})
:将子任务的优先级K[v]
和编号v
加入任务队列q
中,表示将子任务加入待执行的任务队列中。
else
:当当前能量值不足以执行队首任务时,跳出循环。
觉得有用的话可以点点赞,支持一下。
如果愿意的话关注一下。会对你有更多的帮助。
每天都会不定时更新哦 >人< 。