[蓝桥杯 2021 省 AB2] 负载均衡
动态维护最值,又依据与某标准值的比较结果对该最值进行操作尤其是删除->堆-优先队列
这个最值可能来自于某个计算的成果或者元素,但它确实是同类里的之"最"
题目描述
有 n n n 台计算机,第 i i i 台计算机的运算能力为 v i v_{i} vi 。
有一系列的任务被指派到各个计算机上,第 i i i 个任务在 a i a_{i} ai 时刻分配,指定计算机编号为 b i b_{i} bi, 耗时为 c i c_{i} ci 且算力消耗为 d i d_{i} di。如果此任务成功分配,将立刻开始运行, 期间持续占用 b i b_{i} bi 号计算机 d i d_{i} di 的算力, 持续 c i c_{i} ci 秒。
对于每次任务分配,如果计算机剩余的运算能力不足则输出 − 1 -1 −1,并取消这次分配,否则输出分配完这个任务后这台计算机的剩余运算能力。
输入格式
输入的第一行包含两个整数 n , m n, m n,m,分别表示计算机数目和要分配的任务数。
第二行包含 n n n 个整数 v 1 , v 2 , ⋯ v n v_{1}, v_{2}, \cdots v_{n} v1,v2,⋯vn,分别表示每个计算机的运算能力。
接下来 m m m 行每行 4 4 4 个整数 a i , b i , c i , d i a_{i}, b_{i}, c_{i}, d_{i} ai,bi,ci,di,意义如上所述。数据保证 a i a_{i} ai 严格递增,即 a i < a i + 1 a_{i}<a_{i+1} ai<ai+1 。
输出格式
输出 m m m 行,每行包含一个数,对应每次任务分配的结果。
样例 #1
样例输入 #1
2 6
5 5
1 1 5 3
2 2 2 6
3 1 2 3
4 1 6 1
5 1 3 3
6 1 3 4
样例输出 #1
2
-1
-1
1
-1
0
提示
【样例说明】
时刻 1 1 1,第 1 1 1 个任务被分配到第 1 1 1 台计算机,耗时为 5 5 5,这个任务时刻 6 6 6 会结束, 占用计算机 1 1 1 的算力 3 3 3。
时刻 2 2 2,第 2 2 2 个任务需要的算力不足,所以分配失败了。
时刻 3 3 3,第 1 1 1 个计算机仍然正在计算第 1 1 1 个任务,剩余算力不足 3 3 3,所以失败。
时刻 4 4 4,第 1 1 1 个计算机仍然正在计算第 1 1 1 个任务,但剩余算力足够,分配后剩余算力 1 1 1。
时刻 5 5 5,第 1 1 1 个计算机仍然正在计算第 1 1 1, 4 4 4 个任务,剩余算力不足 4 4 4,失败。
时刻 6 6 6,第 1 1 1 个计算机仍然正在计算第 4 4 4 个任务,剩余算力足够,且恰好用完。
【评测用例规模与约定】
对于 20 % 20 \% 20% 的评测用例, n , m ≤ 200 n, m \leq 200 n,m≤200。
对于 40 % 40 \% 40% 的评测用例, n , m ≤ 2000 n, m \leq 2000 n,m≤2000。
对于所有评测用例, 1 ≤ n , m ≤ 2 × 1 0 5 , 1 ≤ a i , c i , d i , v i ≤ 1 0 9 , 1 ≤ b i ≤ n 1 \leq n, m \leq 2\times 10^5,1 \leq a_{i}, c_{i}, d_{i}, v_{i} \leq 10^{9}, 1 \leq b_{i} \leq n 1≤n,m≤2×105,1≤ai,ci,di,vi≤109,1≤bi≤n。
蓝桥杯 2021 第二轮省赛 A 组 H 题(B 组 I 题)。
思路
动态维护最值问题|“弹出”:
算法的复杂度差异主要在任务的弹出上 . 题目只要求输出每一次的 b 机器的情况,所以我们可以只去处理当前的b 机器
需要输出每一次派发任务后的信息,而不是每时每刻,所以我们以每一个任务为单位,进行处理。(任务完毕,则输出,则以每次输入的任务为单位进行结果的输出)
基于任务,挪开任务的依据是其结束时间和当轮任务到来的时刻比较,是要一一比较所有的任务吗?不是的,而是在一个有序区间里面去比较最快,题目有一个特点,就是每当我执行一项输入操作,都会导致容器里的元素最大化的满足我给定的规则,就是while除掉当前驻留的任务.while的依据当然是有序区间上操作
这个有序区间是自己sort,还是交给算法去维护呢?当然应该是算法更高效
AI怎么说
算法如何维护呢?动态维护,就是把该任务加入进去的时候就依据结束时刻动态的进行排序 . 为何不根据map,set这些associative
container呢?set首先不可能,因为其按value排序,而任务的加入,任务本身作为元素就有多个数据域(结束时间,占用算力)
并且set/map对于最值的删除操作,性能不如优先队列:那我什么时候采用priority_queue这个容器(大小顶堆),来维护数据的有序,是较于你说的set/map/multimap/multiset性能更好(主要是时间复杂度)?或者时机更好?
使用priority_queue(大小顶堆)来维护数据有序的时机和性能比较,与set、map、multimap和multiset等其他自动排序容器有所不同。
性能:
时间复杂度: priority_queue通常在插入和删除元素时具有较好的性能。插入操作的时间复杂度为O(log
n),删除操作(弹出顶部元素)的时间复杂度也是O(log n)。然而,对于set、map等容器,插入和查找操作的时间复杂度通常为O(log
n)。 空间复杂度:
priority_queue的空间复杂度通常为O(n),因为它需要额外的空间来维护堆结构。而其他自动排序容器(如set、map等)的空间复杂度通常为O(1)。
适用时机:需求:
priority_queue适用于需要快速访问和删除最大(或最小)元素的情况。如果你经常需要快速获取最大值或最小值,并且需要保持这个顺序,那么使用priority_queue可能更合适。
使用场景:
priority_queue通常用于优先队列、堆排序等场景。例如,你可能有一个任务队列,每次需要执行最高优先级的任务,那么可以使用优先队列来维护这个队列。
其他情况: 如果你需要一个完整的有序集合或映射,并且经常进行查找操作,那么使用set、map等自动排序容器可能更为合适。
总结:选择使用哪种容器取决于你的具体需求。如果你需要快速获取最大(或最小)元素并保持这个顺序,那么priority_queue可能是更好的选择。如果你需要一个完整的有序集合或映射,并且经常进行查找操作,那么使用其他自动排序容器可能更为合适。我还是不明白,那既然set/map/multimap/multiset也可以维持有序性,也可以进行最大最小值的查找和删除,并且我添加东西进去也是自动维持有序的.为什么我还要采用优先队列?
你提到的set、map、multimap和multiset确实可以维持元素的顺序,并且可以进行最大和最小值的查找和删除。然而,优先队列(priority_queue)与这些容器的使用场景和功能有一些关键的区别:
主要用途:
自动排序容器(如set、map等):主要用于存储有序的元素集合,并提供快速的查找和删除操作(自动排序和快速查找)。这些容器在插入时自动排序,允许你快速查找特定元素或根据元素的排序顺序进行操作。set容器提供了随机访问迭代器,因此你可以使用迭代器直接访问set中的元素。然而set容器内部是按顺序存储元素的,所以随机访问的时间复杂度并不是O(1),而是接近O(log
n)。
优先队列:(快速获取和删除最高优先级元素)当向优先级队列中插入元素时,它会根据元素的优先级自动进行排序。优先级队列的主要目的是快速获取并删除优先级最高的元素。主要用于存储一组元素,但这些元素不是简单地按顺序排列,而是根据某个特定的优先级或键值进行排序。优先队列主要用于快速获取并删除优先级最高的元素。
操作:自动排序容器:允许你快速查找、插入和删除元素。它们通常提供稳定的迭代器,使你可以遍历容器中的元素。
优先队列:主要提供push、pop和top操作。push用于向队列中添加元素,pop用于删除并返回优先级最高的元素,top则返回优先级最高的元素但不删除它。
性能:自动排序容器:在插入、查找和删除操作上通常有较好的性能,特别是对于有序数据集。
优先队列:在插入操作上可能稍慢一些(与自动排序容器相比),但在删除优先级最高元素时通常有更好的性能。 适用场景:自动排序容器:适用于需要保持元素有序并且经常进行查找操作的场景,如查找最大或最小值。
优先队列:适用于需要快速获取并处理最高优先级任务的场景,如任务调度、网络流量控制等。
总结:尽管set、map、multimap和multiset等自动排序容器可以维持元素的顺序并提供快速查找和删除操作,但优先队列更适合处理具有优先级概念的场景,其中需要快速获取和删除最高优先级的元素。选择使用哪种容器取决于你的具体需求和应用场景。
显而易见应该以任务结束时间先后弹出,题目并没有保证结束时间的先后与输入先后的关系,我们就应该动态维护最小值并且在其上进行操作,自然就有了堆。
考虑用n个堆维护已在各个计算机中执行的程序,当一个任务来的时候,先将已完成的删除(复杂度体现在这:早完成的任务一定先于晚完成的删除,最值删除,小顶堆),这样每个任务入队后平均只会被访问两次.而不是n-1次
并且,一个任务完成了始终驻留,每次该计算机来新任务我们还每次去重新判定一下,浪费时间,因此,我们可以在再次用到这个计算机前,一次性把所有完成了的任务挪开。
CODE
#include<bits/stdc++.h>
using namespace std;
struct task{//任务
int end_time,consume_power;//任务结束时刻,任务占用算力
friend bool operator<(task a,task b){//重载小于,friend关键字用于声明一个友元函数,友元结构体,友元类,该函数或结构体或类可以访问结构体的私有成员或保护成员。
//这里的operator<是一个比较函数不属于这个结构体,但需要访问task结构体的私有成员来比较两个task对象。因为只能由类/结构体的成员函数,或者友元类/结构体/非该类该结构体的友元函数访问这里面的私有成员。
return a.end_time>b.end_time;//小顶堆用大于,且任务结构体比较的是结束时间,根节点是结束时间最短的
}
};
priority_queue<task>task_heap[200005];
int machine_power[200005];
int main(){
ios::sync_with_stdio(0);cin.tie(nullptr);cout.tie(nullptr);
int n,m;cin>>n>>m;
for(int i=1;i<=n;++i)cin>>machine_power[i];//输入计算机对应的算力
//遍历每一个任务,时间片轮转
for(int i=1;i<=m;++i){
int t_ready_time; //任务启动时间
int index; //任务指定机子
int time_consume; //任务时间消耗
int power_consume; //任务算力消耗
cin>>t_ready_time>>index>>time_consume>>power_consume;
//挪走所有的,直至该任务到来时机子上驻留的,已完成的任务(标志:截止时间小于现在)
while(!task_heap[index].empty()){//优先队列不为空,表示里面还有任务
if(task_heap[index].top().end_time <= t_ready_time){
//如果结束时刻最短的任务的结束时刻小于该任务的启动时刻,则表示该任务的结束时间已经过去,需要挪开位置
//注意,本题的每个任务结束了依然持有算力,所以我们需要回收它占用的算力;
machine_power[index]+=task_heap[index].top().consume_power;
//将该任务移除,以免下次返回小顶堆的堆顶时又得到它,误判.
task_heap[index].pop();
}
//但是,如果有任务,但是还没结束,那么就不要挪开它,只能看它有没有吃剩的算力
else break;
}
//如果这个机子剩余算力 不足以这轮这个任务的开销 那么返回-1 该任务阻塞
if(machine_power[index]<power_consume)cout<<-1<<endl;
//否则,分配剩余算力
else{
task_heap[index].push(task{time_consume + t_ready_time , power_consume});//将该任务加入任务堆(截止时间=启动时间 加 执行时间 , 占用算力)
cout<<(machine_power[index]-=power_consume)<<endl;//重新计算该机子的剩余算力并输出
}
}
return 0;
}
CODE2: By 洛谷 题解人Andy1262
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> P;//结束时间first 占用算力second
int n,m,h[200005];
priority_queue<P,vector<P>,greater<P> > pq[200005];//堆
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>h[i];
while(m--){
int a,b,c,d;
cin>>a>>b>>c>>d;
while(!pq[b].empty() && pq[b].top().first<=a){//将已完成的任务删除
h[b]+=pq[b].top().second;
pq[b].pop();
}
if(h[b]<d) cout<<-1<<endl;
else{//分配
h[b]-=d;
pq[b].push(P(a+c,d));//注意是结束时间
cout<<h[b]<<endl;
}
}
return 0;
}