题目:621. 任务调度器
类型:贪心算法
给定一个用字符数组表示的 CPU 需要执行的任务列表。其中包含使用大写的 A - Z 字母表示的26 种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。CPU 在任何一个单位时间内都可以执行一个任务,或者在待命状态。
然而,两个相同种类的任务之间必须有长度为 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的最短时间。
示例 1:
输入: tasks = ["A","A","A","B","B","B"], n = 2 输出: 8 执行顺序: A -> B -> (待命) -> A -> B -> (待命) -> A -> B.
注:
- 任务的总个数为 [1, 10000]。
- n 的取值范围为 [0, 100]。
解题思路:
这道题,我曾经半年之前刷过,记得当时是做了正正一个上午,完全被他的逻辑要晕了,后面也是一直跟着用例一点儿一点儿的打补丁,最后的代码很丑很丑。时隔半年,我今天又见到了它,本来我已经做好了跟它耗一个晚上的打算,结果出乎意料的是居然一次就通过了,还是有点小惊喜的,因为其实我本身解题思路已经忘得差不多了。
言归正传,解法一是我自己的代码,这道题我的思路是先将给定的字符数组进行统计,将输入数组进行一次扫描将所有字符的出现次数统计到一个数组中保存,同时保存所有字符的总数和字符种类总数。因为当一个字符的出现次数越多的时候它就越容易和前一个相同的字符挨上,整个时间的长度也主要是根据出现次数最多的那个字符所决定的,所以根据贪心算法,我们需要每次都最优先消耗数量最多的那些字符,这样才能保证它们不会被留到最后而导致只能通过增加待命来将它们隔离开。因此我将统计次数的数组按字符的出现次数进行降序排列,即将数量较多的字符放在前面,首先消耗它们。
循环的外层是根据开头字符的出现次数是否为零来判断的,因为每次内部循环结束后我们都进行一次数组的降序排列,如果首字符的次数已经为零则证明全部字符都已经被安排妥当了。内部循环以给定的 n+1 为一个周期,即 n+1 为一组执行时间。在这一组循环中,首先判断是否当前还剩余字符,如果还剩余字符那么安排字符也好,用待命来占位也好,总的时间都一定是要增加一个时间周期的,反之如果发现所有字符都被安排妥当,则直接结束即可。然后安排出现频率较大的字符,每当消耗一个字符则字符的总数减一,同时统计数组中做相应的更新,但这里我们需要加一个限制条件,即当 A = 3 B = 3 n = 2 这种情况的时候,我们会发现这时是三个字符为一个周期,但我们只剩余两种可安排的字符,要符合题意的话必须使用一个待命来占位,这时就需要增加一个判断条件即字符要被安排的位置不能够大于可用字符种类数,否则会数组越位,同时同理例如 A3 B2 C1 n = 2 这种情况,第二次循环的时候就变成了 A2 B1 C0 n = 2 ,这时很明显 C 已经被用光了,所以就不能够再被使用了。
解法二是实例代码,先记在这里,回头来分析。
代码:
//解法一:
void insert(vector<pair<char, int>>& array, char ch){
//用于统计每个字符出现次数的函数
int len = array.size(), res = 0;
for(res = 0; res < len; res++)
if(array[res].first == ch){
array[res].second++;
break;
}
if(res == len) array.push_back(make_pair(ch, 1));
}
int leastInterval(vector<char>& tasks, int n) {
if(tasks.empty()) return 0;
vector<pair<char, int>> times;
int res = 0, start = 0, len = 0, size = 0;
//size 为总字符的个数
size = tasks.size();
for(char task: tasks)
insert(times, task);
//len 为出现的字符种类个数
len = times.size();
//将数组按照字符出现的频率降序排列
sort(times.begin(), times.end(), [](pair<char, int> a, pair<char, int> b){return a.second > b.second;});
//因为数组按字符的出现次数降序排列,当第一位的出现次数为零时
//说明所有的字符都被安排了
while(times[0].second){
for(int j = 0; j < n+1; j++){
//如果还有剩余的字符没有安排则增加一个单位时间,否则直接结束
if(size) res++;
else break;
//如果当前将要安排的位置没有超过可安排的字符的种类总数
//同时要安排的这个字符还有剩余则安排他
//否则用待命来占位
if(j < len && times[j].second){
times[j].second--;
size--;
}
}
//每次完成一个工作周期即重新排序,保证频率高的字符永远在前面
sort(times.begin(), times.end(), [](pair<char, int> a, pair<char, int> b){return a.second > b.second;});
}
return res;
}
//解法二:
int countLeast(vector<unsigned int>& list, int n, int task_num)
{
int max_times = list.back();
if(task_num >= (n + 1) * max_times)
return task_num;
list.pop_back();
int result = (n + 1) * (max_times - 1) + 1;
if(list.size() > 0)
{
int res = countLeast(list, n - 1, task_num - max_times);
if (res >= n * (max_times - 1))
result += (res - (n * (max_times - 1)));
}
return result;
}
int leastInterval(vector<char>& tasks, int n) {
vector<unsigned int> tmp(26);
for(char task : tasks)
{
tmp[task - 'A']++;
}
int task_num = 0;
vector<unsigned int> list;
for(unsigned int k : tmp)
{
if(k > 0)
{
list.push_back(k);
task_num += k;
}
}
sort(list.begin(), list.end());
return countLeast(list, n, task_num);
}