欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

621. 任务调度器

程序员文章站 2022-07-12 12:56:22
...

12月份的题目怎么一直都是贪心题。。

第一次尝试 暴力模拟 TLE

很明显的贪心,求最短时间
相同的字母之间执行顺序需要等待n的时间,不能连续执行
n的范围是0到100
还是要用到hash table来记录任务种类,key是字母,value是次数

直观的想法是,当我们选择了一个任务,标记它被选择的时间
然后当我们需要选择任务的时候,寻找一个剩余次数最多的任务,如果它标记的时间大于n则执行,如果没有大于n那么寻找次多的任务,重复上述步骤直到能选择一个任务执行,如果都不行,那么轮空

下面整理这个思路

  1. 首先用Counter计数,然后再用一个Counter来标记每个元素种类的最后运行时间
  2. 然后用t标记当前时间,默认时间为n,然后我们尝试选择一个剩余次数最多的任务执行
  3. 如果选择元素的最后运行时间-t大于n,那么我们执行它,否则选择次多的元素执行,交替上述过程,直到选择一个任务执行
  4. 执行后,该任务数-1,最后运行时间更新为t,如果没有可执行的任务,轮空
  5. t+=1
  6. 重复上述过程,直到没有任务需要执行

综上所述,我们至少需要两个hash table,一个用来存放次数,一个用来存放最后运行时间,然后我们选择的策略是从多到少,于是我们还需要一个大根堆来存放次数最多的元素,一共是三种数据结构

然后我们可以根据上面的步骤写出代码

class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnts = Counter(tasks)
        # 小根堆,取-v,出现次数最多的位于堆顶,k是任务
        que = [(-v, k) for k, v in cnts.items()]
        heapq.heapify(que)
        # 默认为-n,最开始时所有任务都能执行
        times = dict((k, -(n + 1)) for k in tasks)
        t = 0

        # 当还有任务在执行
        while cnts.keys():
            res = []
            # 尝试选择一个任务执行,最坏的情况就是遍历que里面所元素都找不到一个能够执行的任务
            run = False
            while que:
                # 选择一个次数最多的任务
                item = heapq.heappop(que)
                v, k = item
                # 如果能够满足时间间隔
                if t - times[k] > n:
                    run = True
                    cnts[k] -= 1
                    # 如果任务数都执行完了
                    if cnts[k] == 0:
                        del cnts[k]
                        del times[k]
                    else:
                        # 更新时间为t
                        times[k] = t
                        # v+1就是任务数-1的意思
                        heapq.heappush(que, (v + 1, k))
                    break
                # 如果没有选中它,那么加入到res队列中,等待循环退出再把res中的的元素取出放到que中
                res.append(item)
            # if not run:
            while res:
                heapq.heappush(que, res.pop())
            t += 1
        return t

然后超时了,暴力枚举应该是可行的,但是自己优化不够好导致了超时
621. 任务调度器

第二次尝试 放置元素 策略不正确

我们考虑将v个任务k按照时间间隔n放置到数组中
给定一个tasks,其中k有4个,i有3个,j有2个,时间间隔n=2
假设有4个k,时间间隔n为2,那么可以有如下放置,恰好满足条件

[k,-,-,k,-,-,k,-,-,k]

如果我们此时还有3个任务i,那么上面的数组可以变为

[k,i,-,k,i,-,k,i,-,k]

假设我们还有2个任务j,那么上面的数组可以变为

[k,i,j,k,i,j,k,i,-,k]

如果没有其它任务,那么我们可以很轻易的求出上面任务调度的最短时间,就是数组长度10

更一般的,考虑一个任务序列,我们构造出hash table记录它们的次数,然后我们选择次数最多的任务A,按照以start为起点、间隔为n放置到数组中,那么任务A就安排完毕,然后我们更新start+1,再选择第二多的元素,然后我们放置到数组中,重复上述步骤,数组的长度就是任务所花的最短时间

为什么优先安排次数多的任务而不是次数少的任务?主要是为了编程方便。。

不过为什么我们不直接返回最多任务数构成的第一次安排的数组的长度呢,这是因为有可能少任务数很多,导致超过了第一次安排数组的长度,例如
假设有4个k,但是有100个个数为1的任务,那么我们第一次仍然安排任务k

[k,-,-,k,-,-,k,-,-,k]

然后我们考虑剩下100个个数为1的任务,那么最终数组长度显然会超过[k,-,-,k,-,-,k,-,-,k]

现在的关键点就是怎么把填充数组这个步骤用高效的方法实现,观察测试样例,最多的task一共有26种,tasks长度最长是100000,然后最大的n是100,那么我们最极端的情况,数组直接开100000*100的长度,考虑到边界情况再乘一个10,也就是 1 0 7 10^7 107的空间,然后我们就能模拟放置的过程了

class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnts = Counter(tasks)
        start, end = 0, 0
        arr = [0] * (10 ** 7)
        que = [(-v, k) for k, v in cnts.items()]
        heapq.heapify(que)
        while que:
            v, k = heapq.heappop(que)
            ind = 0
            for ind in range(-v):
                arr[start + ind * (n + 1)] = k

            # 更新下标
            end = max(end, start + (- (v + 1)) * (n + 1))

            # 寻找下一个起点
            while arr[start] != 0:
                start += 1
        # print(arr[:end + 1])
        return end + a

上面这段代码交上去虽然最长的那个序列通过了,但是在被这个样例卡住了。。

[“A”,“A”,“A”,“B”,“B”,“B”, “C”,“C”,“C”, “D”, “D”, “E”]
2

这个样例最短的序列是12,能够构造出如下长度为12的序列

[DABCABCABCDE]

不需要等待

但是我们按照上面的代码输出结果是13

[‘A’, ‘B’, ‘C’, ‘A’, ‘B’, ‘C’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, 0, ‘D’]

需要等待1次

从结果上说明这个贪心的策略不正确,那不正确的策略没必要再改了

第三次尝试 暴力模拟 通过

回过头来改进第一次尝试的方法,原先的两个hash表+1个小根堆有点略显复杂了,现在想办法怎么把选取操作变得更简单一些

看了一下题解的方法一,也是同样的模拟思路,不过是换了一种角度存储time,现在尝试把小根堆去掉,再看看执行还会超时吗

emmm…去掉了小根堆之后反而没超时,看来是小根堆的pop和push操作代价太高了。。

class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnts = Counter(tasks)
        # value是指当前元素可以运行的时间点
        times = dict((k, 0) for k in tasks)

        t = 0
        # 当还有任务需要执行
        while cnts.keys():
            # 策略:选择一个次数最多的元素执行
            maxv = 0
            key = None
            for k, v in cnts.items():
                if maxv < v and times[k] <= t:
                    maxv = v
                    key = k
            if key:
                times[key] += (n + 1)
                cnts[key] -= 1
                if cnts[key] == 0:
                    del times[key]
                    del cnts[key]
            t += 1
        return t

贴一下官方题解,它这里没有用两个哈希表,只用了一个哈希表和两个list,然后time的更新是跳跃前进的,不是每次+1,这样就能保证每一次循环都能选择一个任务执行

class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        freq = collections.Counter(tasks)

        # 任务总数
        m = len(freq)
        nextValid = [1] * m
        rest = list(freq.values())

        time = 0
        for i in range(len(tasks)):
            time += 1
            minNextValid = min(nextValid[j] for j in range(m) if rest[j] > 0)
            time = max(time, minNextValid)
            
            best = -1
            for j in range(m):
                if rest[j] and nextValid[j] <= time:
                    if best == -1 or rest[j] > rest[best]:
                        best = j
            
            nextValid[best] = time + n + 1
            rest[best] -= 1

        return time

不过执行速度还是很慢
621. 任务调度器
621. 任务调度器

题解 构造

官方题解说得很清楚了,这里略
简要的说一下思想,就是在第二次尝试的放置方法中,不是简单的选取并放置,而是推出了数学公式

相关标签: leetcode