[230603 lc621] 任务调度器
写在前面:so sad,今天状态很不好,这道题看了一下午加一晚上,屏幕上的字它不进脑子啊!还是深夜效率高!君独何为至于此,山非山兮水非水。
一 题目
给你一个用字符数组 tasks
表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。
然而,两个 相同种类 的任务之间必须有长度为整数 n
的冷却时间,因此至少有连续 n
个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的 最短时间 。
示例 1:
输入:tasks = ["A","A","A","B","B","B"], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。
示例 2:
输入:tasks = ["A","A","A","B","B","B"], n = 0
输出:6
解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0
["A","A","A","B","B","B"]
["A","B","A","B","A","B"]
["B","B","B","A","A","A"]
...
诸如此类
示例 3:
输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
输出:16
解释:一种可能的解决方案是:
A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A
提示:
1 <= task.length <= 104
tasks[i]
是大写英文字母n
的取值范围为[0, 100]
二 模拟法(vector)
2.1 整体思路
模拟法顾名思义,模拟人脑解决此题的流程。对每类任务设置两个属性:可执行时间和剩余次数。可执行时间指该任务冷却后最早可执行的时刻;剩余次数指该任务还需执行几次。
我们使用贪心的思想:利用 time 来记录当前时间,每次选择可执行时间小于等于当前 time 并且剩余次数最多的任务执行,然后更新该任务的可执行时间和剩余次数。
此法中利用 vector 来记录两个属性。
2.2 关键点
易错点在:遍历搜索最多剩余次数的任务时,使用 ==
会出现 runtime error
,要使用 <=
。虽然我不知道为啥但还是记住这样做吧。
2.3 代码分析
class Solution {
public:
int leastInterval(vector<char>& tasks, int n) {
//统计频率
vector<int> freq(26, 0);
for(char ch: tasks) {
freq[ch - 'A']++;
}
//构造记录两个属性的vector:初始时每类任务的可执行时间均为1
vector<pair<int, int>> record;
for(int i = 0; i < freq.size(); ++i) {
if(freq[0]) {
record.push_back({1, freq[i]});
}
}
//开始逐时间执行任务
int time = 0;
int size = tasks.size();
for(int i = 0; i < size; ++i) {
++time;
//选择当前的最小可执行时间
int minTime = INT32_MAX;
for(int j = 0; j < record.size(); ++j) {
if(record[j].second) {
minTime = min(minTime, record[j].first);
}
}
//因为最小的可执行时间可能大于当前时间,此时说明会出现空白时间间隔
time = max(time, minTime);
//选择当前可执行时间下剩余次数最多的任务执行
int index = -1;
for(int j = 0; j < record.size(); ++j) {
if(record[j].second && record[j].first <= time) {
if(index == -1 || record[j].second < record[index].second) {
index = j;
}
}
}
//在该时间片记录第index任务,并更新它的两个属性
record[index].first = record[index].first + n + 1;
--record[index].second;
}
return time;
}
};
三 模拟法(优先级队列/堆)
3.1 整体思路
整体思路和使用 vector 的模拟法类似,每次选择可执行时间最小且剩余次数最大的任务优先执行。和上一个做法的不同在于,使用优先级队列来帮助我们排序,每次取堆顶元素执行即可,然后更新该栈顶元素的两个属性,若还需继续执行,则将其压入堆中,直至堆为空,全部任务执行完毕。
3.2 关键点
关键点在优先级队列仿函数的书写,但不太难。
3.3 代码分析
class Solution {
private:
class Compare{
public:
//first为可执行时间,优先可执行时间小的
//second为剩余次数,在可执行时间相同的情况下,优先剩余次数大的
bool operator()(pair<int, int> a, pair<int, int> b) {
if(a.first != b.first) {
return a.first > b.first;
}
return a.second < b.second;
}
};
public:
int leastInterval(vector<char>& tasks, int n) {
//首先统计频率
vector<int> freq(26, 0);
for(char ch: tasks) {
freq[ch - 'A']++;
}
//定义优先级队列
priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> que;
for(int num: freq) {
if(num) {
que.push({1, num});
}
}
//开始执行任务
int time = 0;
while(!que.empty()) {
++time;
pair<int, int> tmp = que.top();
que.pop();
time = max(time, tmp.first);
//执行任务并更新属性
tmp.first = tmp.first + n + 1;
tmp.second--;
if(tmp.second) {
que.push(tmp);
}
}
return time;
}
};
四 桶思想
4.1 整体思路
假设最高频率为 q,那么我们设置 q 个桶,并且前 (q - 1) 个桶大小为 (n + 1),因为对于最高频率的任务来说,执行完该任务至少需要 (q - 1) * (n + 1) + 1 的时长。然后我们统计,有多少个频率为 q 的任务,逐个增加最后一个桶的大小。
4.2 关键点
关键点在理解桶的思想,计算最后一个桶的大小。
4.3 代码分析
class Solution {
private:
static bool compare(int& a, int& b) {
return a > b;
}
public:
int leastInterval(vector<char>& tasks, int n) {
vector<int> freq(26, 0);
for(char ch: tasks) {
++freq[ch - 'A'];
}
sort(freq.begin(), freq.end(), compare);
int lastSize = 1;
for(int i = 1; i < freq.size(); ++i) {
if(freq[i] == freq[0]) {
++lastSize;
} else {
break;
}
}
return max(int(tasks.size()), (freq[0] - 1) * (n + 1) + lastSize);
}
};