目录
- 前言
- 基本问题建模
- 单机环境
- 复杂环境:并行多处理机与工厂模型
- 基于优先级的贪心策略
- 单机调度模式
- 平均带权完成时间:\(1||\sum w_jC_j\)
- 最大延时:\(1||L_{max}\)
- 抢占式调度与发布时间:\(1|r_j,pmtn|f\)
- 双机流水线模式
- 并行处理机模式
- 抢占式:\(P|pmtn|C_{max}\)
- 非抢占式:\(P||C_{max}\)
- 优先级策略的限制
- 单机调度模式
- 更精妙的策略
- \(1||f_{max}\)的通用贪心解
- Least Cost Last
- 扩展到\(1|prec|f_{max}\)
- 另一种解法
- \(1||\sum w_jU_j\)的动态规划解
- \(P||C_{max}\)的动态规划解
- \(1||f_{max}\)的通用贪心解
- 图与线性规划
- 二分图匹配
- \(R||\sum C_j\)问题
- \(O|pmtn|C_{max}\)问题
- 二分图匹配
漫长的“寒假”,随便放点读书笔记,长文慎读,整理自《Algorithms and Theory of Computation Handbook》。
前言
几个简单场景:
一个CPU需要处理不断到达的程序。如何安排程序处理的顺序,最小化程序的平均处理时间(任务到达至完成的时间)。
考虑一队五个宇航员准备重返太空。有一些任务需要在出发前完成。每个任务必须被分配给一个宇航员,且有的任务需要在其他任务完成后才能开始。如何安排任务分配,使得所有任务的完成时间最小。
考虑一个生产不同类型产品的工厂。不同产品需要不同机器上的不同处理时间,需要首先在机器1处理,然后是机器2,最后机器3,不同产品在不同机器上处理时间不同。工厂会收到订单,每个订单有额定的完成时间,必须在此之前完成。如何安排机器的生产顺序,使得工厂完成尽可能多的订单。
更一般地说,调度问题通常指一系列任务需要分配到一些机器上,满足某些约束,并且优化一个特定的目标函数。从各种各样的应用课题中可以挖掘出成千上万的调度问题建模,因此几乎不可能完全覆盖这些问题。因此,我们将从一些基本问题出发,介绍一些对解决调度问题有用的算法设计技术。
我们仅聚焦于多项式时间算法,但是实际上许多这类问题都是NP难问题,显然很难有确定性的多项式时间算法来得出最优解。在这些问题中,我们通常对相关的近似算法感兴趣。
基本问题建模
一个调度问题通常有三个元素:机器环境,最优条件,其余边界约束。我们首先从最简单的机器环境开始,然后再讨论更复杂的。
单机环境
假设有\(n\)个任务的任务集合\(J\),在单机环境中,有一个在同一时间只能处理单个任务的处理机。任务\(j\)需要的处理时间为\(p_j\),如果一个任务一旦开始就必须直接执行完,则称该调度环境是非抢占式的,否则是抢占式调度。对任务集\(J\)的一个调度\(S\),指定机器何时调出\(p_j\)的时间来执行任务\(j\),任务\(j\)在调度\(S\)中的完成时间记为\(C_j^S\)。
一个调度算法的目标是计算一个“好”的调度策略,但是“好”的定义取决于不同的应用。必须要确定一个最优性条件,调度算法的最终目标是给出满足该条件的调度策略。下面考虑一些边界约束,每个任务\(j\)具有发布时间\(r_j\),只有在任务发布后才能被处理。同时任务之间还有偏序\(<\),只有所有满足\(j'
现在可以考虑一个简单的问题模型\(\alpha|\beta|\gamma\),在单机问题中,\(\alpha=1\)代表单机环境,\(\beta\)指边界约束,\(\gamma\)指优化目标。如果要优化所有任务的平均完成时间,则\(\gamma=\sum C_j\),若是追求所有任务的完成时间,\(\gamma=C_{max}\)。在当前环境下,\(\beta\)是\(r_j,prec,pmtn\)的子集,分别代表发布时间,任务间的偏序约束,是否抢占式调度。上面提到的场景1就可以用\(1||\sum C_j\)来建模。
此外,还有两个其他可能的元素会影响单机调度环境。
- 任务有不同的优先级,某些任务需要被优先处理,因此为任务\(j\)分配一个可能的权重\(w_j\),权重越大,其完成时间相关惩罚就越大,因此\(\gamma=\sum w_jC_j\)。
- 任务\(j\)有一个截止时间\(d_j\),规定它应该被完成的时间,这会导致两种不同的优化函数。对于调度策略\(S\),定义\(L_j=C_j^S-d_j\)为任务延时,我们可以选择最小化\(n\)个任务的最大延时:\(\gamma=L_{max}\)。也可以选择最大化在截止时间之前完成的任务的数量,据此可以定义\(U_j=0\)若\(C_j^S\le d_j\),否则\(U_j=1\)。此时可以定义\(\gamma=\sum U_j\),当然也可以加上权重\(\sum w_jU_j\)。
最后,我们考虑一个更一般的优化条件。对任务\(j\),定义\(f_j(t)\)为随完成时间\(t\)的非减函数。对调度策略\(S\),定义\(f_{max}=\max_{j=1}^nf_j(C_j^S)\)。后面会考虑到的一个问题建模就是\(1|prec|f_{max}\)。
复杂环境:并行多处理机与工厂模型
现在我们考虑一些更复杂的环境,首先是并行机器环境。假设有\(m\)个机器,任务\(j\)可被任意一个机器处理,当然如果允许抢占式调度,那么一个任务可以先在一个机器上处理,再转移到另一个。单个机器同时只能处理不多于一个任务,单个任务同时只能在不多于一个机器上运行。
在等价(identical)并行环境中,每个处理机完全相等。在均匀相关(uniformly related)并行环境中,每个机器\(i\)具有速度\(s_i\),因此任务\(j\)在机器\(i\)上的运行时间将是\(p_j/s_i\)。在不相关(unrelated)环境中,每个机器的速度还和任务相关,即\(s_{ij}\),表示机器\(i\)运行任务\(j\)的速度,此时运行时间将为\(p_{ij}=p_j/s_{ij}\)。
在工厂环境中,也有\(m\)个机器,但是任务由不同的操作组成,每一个操作需要在特定的机器上完成,不同操作可能需要不同的时间来完成。在开放工厂(open shop)中,每个商品的不同处理操作可以以任意顺序完成,只要保证两个操作不是同时在不同机器上处理的。而在任务工厂(job shop)中,不同的操作之间具有一个全序关系,一个操作必须在其所有前驱操作完成后才被执行。任务工厂的一个特殊例子是流水线工厂(flow shop),它还要求不同任务在机器上的处理时间不同。在流工厂和开放工厂中,每个任务都需要在每个机器上处理一次,完成特定操作。
上面介绍的等价并行,均匀相关,不相关环境分别用\(P\), \(Q\)和\(R\)来表示,开放工厂,流水线工厂,任务工厂分别用\(O\),\(F\),\(J\)表示。至此前面提到的简单场景2可以用\(P5|prec|C_{max}\)建模,场景3可用\(F3|r_j|\sum U_j\)建模。
基于优先级的贪心策略
最显然的解决上述问题的调度算法就是贪心选择:每当一个机器空闲下来,就分配给它一个任务。我们把算法再稍微设计的精致一点的话,就为每个任务定义一个优先级函数(根据最优条件\(\gamma\)),然后分配时分配最高优先级的任务。在这一章节,我们讨论单机环境,多机并行环境,工厂环境下如何设计这样的贪心策略。在设计的时候,优先级函数仅与当前任务\(j\)相关,这样的话调度算法仅需要两步:计算优先级,排序。显然时间复杂度为\(O(n\log n)\)。我们也将探讨这样设计的一些缺陷。
单机调度模式
首先关注单机调度问题下的优先级策略:按照优先级对任务进行排序,而后以此顺序作为调度策略。如果要证明此类算法的最优性,通常采用交换的方法来论证:如果存在一个最优策略,其执行顺序不符合按照优先级排序的结果,也就说明这个顺序中存在两个任务顺序与优先级不符,我们证明交换这两个任务能带来更好的结果,以此说明这样的最优策略不存在(也就是常见的反证)。
平均带权完成时间:\(1||\sum w_jC_j\)
这大概是最简单的模型了,优化所有任务的完成时间之和\(\sum C_j\)。直觉上看,我们应该把花费时间最长的任务放在最后做,这样其花费就不会累加在其余任务之上,这就是最短任务优先算法(shortest processing time):按处理时间\(p_i\)非减地排序所有任务,按照这个顺序来调度任务。
定理3.1.1:\(SPT\)算法可以最优的解决\(1||\sum C_j\)调度问题。
证明:假设存在两个任务\(j\)和\(k\)按照相邻顺序参与调度,但是\(p_j>p_k\),形成了一个最优调度。现在我们将两个任务对调,此时由于\(j\)和\(k\)相邻,其他任务的完成时间不变,唯一变化的是\(j\)和\(k\),假设两个任务开始于时间\(t\),则对调前,两个任务的完成时间之和为\((t+p_j)+(t+p_j+p_k)\),对调后,完成时间为\((t+p_k)+(t+p_k+p_j)\),变化为\(p_k-p_j\),由于\(p_j>p_k\),因此对调后完成时间更优,与假设的最优性矛盾。
实际上对于带权的问题\(1||\sum w_jC_j\)也可以按照这个思路来证明,直觉上我们要充分利用每一段单位时间的权重,单位时间内获得的权重越大越好,也就是按照\(w_j/p_j\)升序排列。其证明可以简单地由上面的证明推演出。
最大延时:\(1||L_{max}\)
一个很自然的想法是,首先调度最紧迫的任务,这催生出最早截止算法(earliest due date)EDD:按任务的截止时间升序排列并调度之。可以证明,EDD对于\(1||L_{max}\)是最优的。
定理3.1.2:\(EDD\)算法对于\(1||L_{max}\)是最优的。
证明:同样适用交换论证法。不失一般性,假设所有任务的截止时间不重复,且\(d_1
抢占式调度与发布时间:\(1|r_j,pmtn|f\)
现在考虑更复杂点的单机调度,不同的任务会在不同的时间到达,即发布时间\(r_j\),显然上面提到的贪心策略不能直接使用,因为优先级高的任务不一定会早于优先级低的任务被发布。要处理这样的情况,最直接的方法就是不管他,每次调度都处理当前已发布的任务中优先级最高的任务。在抢占式调度中,这意味着一旦一个更高优先级的任务发布,当前正在执行的任务就要挂起,把处理器资源让出来。我们将会证明这个策略在抢占式调度中是最优的。
定义最短剩余时间优先算法(shortest remaining processing time, \(SRPT\)):每个时间点按照最短剩余时间调度,当有剩余时间小于当前任务时,抢占处理机。同时定义新的EDD算法:按照当前已发布的最早截止时间任务抢占式调度。可以证明,\(SRPT\)是调度\(1|r_j,pmtn|\sum C_j\)问题的最优算法,\(EDD\)也是\(1|r_j,pmtn|L_{max}\)的最优解。
证明:和之前一样采用反证法导出矛盾,不同的是我们这次交换的是不同任务的片段(考虑到抢占式调度)。首先我们考虑\(1|r_j,pmtn|\sum C_j\)问题,假设一个最优调度中具有最短剩余时间的已发布任务\(j\)没有在\(t\)时刻被调度,而是任务\(k\)被调度,且剩余时间\(p_j'
之前考虑的都是抢占式调度下,具有发布时间\(R\)的调度策略,分别是\(SRPT\)和\(EDD\)。但是对于非抢占式调度,由于不能简单地挂起当前任务执行新任务,我们必须决定要不要停机等待一个更高优先级任务,还是直接执行面前的低优先级任务,这在直觉上很难计算最优解。实际上,\(1|r_j|\sum C_j\)和\(1|r_j|L_{max}\)都已被证明为\(NP\)难问题,我们将在后续介绍相关的近似算法。
同样地,上面的贪心策略也不适用于带权重的任务调度,\(1|r_j,pmtm|\sum w_jC_j\)也被证明为\(NPH\)问题。同时\(SRPT\)和\(EDD\)称为在线调度算法,因为它们不依赖于当前时刻未知的条件,仅依赖当前已发布的任务的优先级来进行调度。文献[38]是一个完整的在线调度综述。
双机流水线模式
另一个更复杂的问题是在流水线工厂中最小化任务的最终完成时间,一般来说,对于大于等于3个机器,这个问题是\(NP\)难的。但是特殊情况,对于双机流水线\(F2||C_{max}\),存在一个基于优先级的最优算法。记\((a_j,b_j)\)为任务\(j\)需要在两个机器上进行处理的时间。直觉上,我们应该让第一个机器尽可能快地处理短任务,以让第二个机器减少等待;同时应该让第二个机器先处理长任务,因为这些任务本身就代表着较大的完成时间,缩小它们有助于缩小最终的\(C_{max}\)。
下面将这个想法形式化:将所有任务分成两个集合,\(A\)表示\(a_j\le b_j\)的任务集合,\(B\)表示\(a_j>b_j\)的任务集合。定义一个\(Johnson\)规则:首先将\(A\)集合中的任务按照\(a_j\)升序排列,再将\(B\)集合中的任务按照\(b_j\)降序排列,按这个顺序调度两个机器上的所有任务。
注意:我们并没有在第二个机器上重排任务,所有任务的两个操作在两个处理机上都是按相同顺序执行,这种调度称为排列调度(permutation schedule)。注意:超过2个机器就不一定有最优排列调度了,可能需要在第二个机器上重排。
定理3.2.1:\(F2||C_{max}\)问题的实例总是存在一个最优的排列调度。
证明:考虑任意一个最优调度\(S\),按照任务在机器1上完成的时间对任务进行编号。假设存在任务对\(j
现在我们可以将问题的解空间限制在排列调度上了(因为存在最优的排列调度)。
定理3.2.2:通过\(Johnson\)规则形成的任务排列是双机流水线模式的最优调度。
证明:注意到,在一个排列调度中,必然存在一个任务\(k\)在机器1的操作完成后可以不经过等待直接开始在机器2上的操作。因此\(n\)个任务的完成时间包含前\(k\)个任务在机器1上的处理时间和\(n-k+1\)个任务在机器2上的处理时间。这些加起来一共包含\(n+1\)个任务的处理时间(\(n+1\)个\(a_i\)或\(b_j\)的组合),因此如果把所有的\(a_i\)和\(b_i\)减去相同的时间\(p\),那么在排列调度下,最终完成时间将减少\((n+1)p\)。
同时,还注意到,如果一个任务的\(a_i\)等于0,它必然在某个最优排列调度中被最优先调度,因为它不占用机器1的时间,反而可以填充机器2可能的空闲等待时间,充分利用机器2。类似的,如果一个任务的\(b_i\)等于0,它必然在某些最优排列中被放在最后执行(因为机器1是无空闲的,而在机器2上不占时间,不能提高机器2的利用率)。
因此,我们可以这样构造一个最优的排列调度:不断寻找未调度任务中具有最小\(a_j\)或\(b_j\)的任务\(j\),然后将所有未调度任务的处理时间减去这个最小值,此时任务\(j\)的\(a_j\)或\(b_j\)为0,按照上面的分析将其置于排列的最前或最后。
容易看出,这样的构造产生的最优排列满足\(Johnson\)规则。
并行处理机模式
现在我们考虑等价并行环境\(P\),机器一旦多起来,很多原先在单机环境下很简单的问题变成了\(NP\)难问题。因此我们倾向于寻找问题的近似算法。实际上,在一些情况下,单机环境下简单的优先级调度也能够在并行环境下得到不错的效果。这类算法的通常步骤是:每当一台机器变得空闲,就把当前优先级最高的任务分配给它。这种不放任任何机器空闲的调度策略被称为“繁忙”调度(busy schedule)。
在这一节,我们还将介绍一个新的分析方法。我们将给出一个最优调度策略质量的下界,而不再使用交换法论证策略最优性。而后我们将论证提出的近似算法能够达到最优策略下界的附近,比如差一个因子。这是分析近似算法的常见技术,保证了近似算法的解能够以比例逼近最优解,虽然我们并不知道最优解的位置。有时我们甚至能论证新的贪心算法能够到达最优的下界,这就意味着该贪心算法实际上是最优的。
首先聚焦于最小化\(m\)个并行机上的平均完成时间,即\(P_m||\sum C_j\)。在这个问题下,贪心的\(SPT\)算法依然是最优的。后面会进行证明。
而后考虑最小化最终完成时间\(C_{max}\),在单机环境下,\(1||C_{max}\)基本不是一个问题,只要保证机器不空闲,随便怎么调度结果都一样。但是机器一旦多起来,问题就很复杂了。如果允许抢占式调度,存在一个最优的多项式时间的贪心算法。但是在非抢占式条件下就不行了,它已被证明是\(NPC\)问题[7],我们将给出一个它的近似算法,首先我们证明任何繁忙调度是2-近似的,然后我们介绍一个更聪明的4/3-近似算法——\(LPT\)算法,最后我们将给出一个更优但也更复杂的算法。
我们对这些算法的分析是基于比较它的解与最优解下界的差距,因此和最优解的差距会更小。下面是两个简单的\(C_{max}\)的下界:
\[ C_{max}^*\ge \sum_{j=1}^n p_j/m \]
\[ C_{max}^*\ge \max_jp_j \]
第一个下界说明,最优调度策略得到的完成时间至少是每个机器的平均负载,第二个下界说明,最终完成时间至少是任何一个任务的完成时间。这两个下界都是很显然的,且对于抢占式和非抢占式都有效。首先对于抢占式问题\(P|pmtn|C_{max}\),我们将给出一个最优调度算法,它能够达到上面两个下界的最大值;后续对于非抢占式调度,我们将使用这个下界来确定近似解。
抢占式:\(P|pmtn|C_{max}\)
McNaughton’s wrap-around rule
\(McNaughton’s\)规则是一个简单的构造\(P|pmtn|C_{max}\)问题最优解的方法,它最多需要\(m-1\)次抢占。这个算法和许多其他调度算法不同,以机器为单位调度任务,而非时间。
定理3.3.1:用\(McNaughton’s\)规则构造\(P|pmtn|C_{max}\)的策略,其最终完成时间达到最优下界。
证明:首先定义\(D\)是最优下界,它等于上述的两个下界的最大值:\(D=max\{\sum_jp_j/m,\max_jp_j\}\)。而后,我们准备把任务片段分配给各个机器:按顺序从机器\(1\)至\(m\),任务\(1\)至\(j\)进行分配,每个机器分配的处理时间都不超过\(D\),机器\(i\)分配满\(D\)时间后才能开始机器\(i+1\)的分配。每个任务\(j\)有最后的\(t\)时间片在机器\(i\)上执行,前\(p_j-t\)时间片在机器\(i+1\)上运行。由于\(D\)不小于单个任务的处理时间,因此一个任务最多被抢占一次分在两个机器上执行。又由于\(mD\)不小于所有任务的总处理时间,按照这种方法分配,所有任务都一定能够被分配成功,这说明会有一个任务不需要抢占(否则需要\(m+1\)个机器),因此该算法共抢占\(m-1\)次。
非抢占式:\(P||C_{max}\)
\(P||C_{max}\)是已证明的\(NP\)难问题,我们给出几个简单的近似算法。
List Scheduling
这是一个极其简单的贪心策略:每当有新的机器空闲,就为其分配任意一个未处理的任务,但它竟然是\(2\)-近似的。
定理3.3.2:\(LS\)算法是\(P||C_{max}\)的\(2\)-近似算法。
证明:假设任务\(j\)是在一次\(LS\)调度中最后完成的任务,那么它的完成时间就是最终完成时间\(C_{max}\),设它的处理时间为\(p_j\),开始时间为\(s_j\),则\(C_{max}=s_{j}+p_j\)。注意到在时间点\(s_j\)之前,所有的机器应该都是繁忙的,否则任务\(j\)将更早开始。而所有机器同时保持繁忙的时间不会超过\(\sum_{j=1}^n p_j/m\),否则的话所有任务都完成了。因此我们有:
\[ C_{max}^{LS}\le s_j+p_j\le \sum_{j=1}^n p_j/m+p_j\le 2C_{max}^* \]
这个算法很简单,其得到的完成时间不超过最优解的两倍,实际上即使任务有发布时间\(r_j\),即对于\(P|r_j|C_{max}\),这个算法也是\(2\)-近似的,证明方法类似,就不赘述了。
Longest Processing Time First
在上面的\(LS\)算法中,我们每次分配任务都是任意选择的,我们在分析的时候选择的瓶颈值是最后一个处理完成的任务,因此其完成时间不超过\(\sum_{j=1}^n p_j/m+p_j\),一个很自然的想法是,让最后一个完成的任务尽可能短,这样完成时间的上界是不是就更接近最优呢?这就是最长处理时间优先调度\(LPT\):在\(LS\)调度的基础上,每次选择处理时间最长的任务进行分配。
定理3.3.3:\(LPT\)算法是\(P||C_{max}\)的\(4/3\)-近似算法。
证明:依然选取最后完成的任务\(j\),它开始于时间点\(s_j\),但它并不一定是最短任务,因为可能有其他任务在\(s_j\)之后开始但是在\(s_j+p_j\)之前完成。因此,我们把所有在\(s_j\)之后开始的任务移除,这不改变最终完成时间。只要对移除后的问题实例分析即可,在新的问题中,任务\(j\)同时也是最后开始的任务,这说明\(p_j=p_{min}\)。
参照\(LS\)算法的分析,\(LPT\)的调度结果\(C_{max}^{LPT}\le C_{max}^*+p_{min}\),如果\(p_{min}\le C_{max}^*/3\),不证自明,因此只需证明\(p_{min}>C_{max}^*/3\)的情形下,\(LPT\)就是最优调度:
在这种情况下,任意的\(p_j>C_{max}^*/3\),意味着最优调度中,每个机器分配的任务数不超过2,假设有\(m\)个机器,\(n\)个任务,这说明\(n\le 2m\),当\(n\le m\),显然最优调度就是每个机器分配一个任务,这和\(LPT\)是一致的,因此考虑另一种情况\(m
带依赖的任务:\(P|prec|C_{max}\)
当输入的任务存在依赖关系时,\(LS\)调度依然是非抢占式调度的一个选择,当然会有一些小修改。我们说一个任务在\(t\)时刻是可运行的,如果其所有前驱均已在\(t\)时刻被完成。在这种情况下,\(LS\)调度将每次选择任意可运行的任务调度给空闲机器。同时,假设任务集中存在的任意一条任务链为\(j_{i_1}
\[ C_{max}^*\ge \max_i\sum_{l=1}^k p_{i_l} \]
这是有依赖关系的任务调度中新增加的下界。
定理3.3.4:\(LS\)算法是\(P|prec|C_{max}\)的\(2\)-近似算法。
证明:假设\(j_1\)是调度策略中最晚完成的任务,定义\(j_2\)是\(j_1\)的前驱任务中最晚完成的任务,如此递归地定义\(j_l\),直到任务\(j_k\)不存在前驱,构成集合\(C=\{j_1,j_2...,j_k\}\),我们把\(LS\)调度策略的时间消耗分成两部分,\(A\)表示所有\(C\)中任务正在某个机器上运行的时间点,\(B\)表示其余的时间点,则\(C_{max}=|A|+|B|\)。注意到:\(B\)中所有时间点上,\(m\)个机器必然都是繁忙状态,否则,如果有机器处于空闲状态,而此时必然存在\(C\)中可运行的任务(\(C\)中没有任务在运行),则该机器应当直接运行该任务。因此根据前面的分析,所有机器同时保持繁忙的时间不会超过\(\sum_{j=1}^n p_j/m\),从而\(C_{max}=|A|+|B|\le \sum_{j\in C}p_j+\sum_{j=1}^n p_j/m\le 2C_{max}^*\)。
LS for \(O||C_{max}\)
\(LS\)调度还可以应用于\(O||C_{max}\)问题,开放工厂问题下,每个任务需要在不相交的时间区间内在不同机器上进行处理。设\(P_{max}\)是单个任务在所有机器上的处理时间之和的最大值,\(\Pi_{max}\)指单个机器上所有任务的处理时间之和的最大值,显然两者均是最优调度的下界。
定理3.3.5:\(LS\)算法是\(O||C_{max}\)的\(2\)-近似算法。
证明:假设机器\(M\)是最后完成处理的机器,\(j\)是机器\(M\)上最后处理的任务。在任何时间点,要么机器\(M\)在处理中,要么任务\(j\)在某个机器上处理,否则任务\(j\)将在机器\(M\)上处理。\(j\)的总处理时间不超过\(P_{max}\),而\(M\)的总处理时间不超过\(\Pi_{max}\),因此\(LS\)调度中,总处理时间最多不超过\(P_{max}+\Pi_{max}\),从而不超过\(2C_{max}^*\)。
优先级策略的限制
对许多问题,简单的调度策略并不能达到很好的效果,因此我们在设计算法的时候,必须小心地选择策略组合。实际上对于很多问题,特别是任务之间存在依赖关系,或是有发布时间的情况,最优调度通常允许机器有空闲时间。而应用贪心策略得到的调度通常是繁忙调度,往往导致次优解。
考虑\(Q||C_{max}\)问题的简单实例:两个任务,两个机器,机器1的运行速度为1,机器2的速度为\(x\),其中\(x>2\),两个任务的处理时间均为1。应用任何贪心调度策略,都会将两个任务分在两个机器上,完成时间为1。但很显然的是,如果我们将两个任务都分在机器2上,完成时间是\(2/x\),更优。当\(x\)无限扩大时,没有任何贪心策略的解能够达到固定的近似比。实际上对于这个问题,我们还是能找到简单的\(2\)-近似启发式算法来解决问题,但是对于类似\(R||C_{max}\)和\(Q|prec|C_{max}\)这类复杂问题,就没有简单的近似算法。
更精妙的策略
基于优先级的调度算法把任务分开来单独看待,在很多问题上会错过最优解。在这一节,我们考虑更复杂的策略,同时考虑其他任务的信息来辅助决策,而不仅仅按照优先级排序。这些算法是递增迭代的,通常从一个空集开始,每次调度一个任务,直到达到最优解。每次任务的选择是根据已分配的任务上下文来计算的。我们将给出两个经典的动态规划算法,以及另外几个更特别的算法。
\(1||f_{max}\)的通用贪心解
我们解决的第一个问题是\(1||f_{max}\),这是之前定义过的一大类问题,每个任务存在一个非减的惩罚函数\(f_j(C_j)\),问题的目标是最小化所有任务的最大惩罚值,例如\(1||L_{max}\)中,\(f_j(t)=t-d_j\)。
Least Cost Last
我们定义\(p(J)=\sum_{j\in J}p_j\)为所有剩余任务的处理时间之和,\(J\)是未调度的任务集合,任何任务都在\(p(J)\)时间之前完成,算法的主体是找到使\(f_j(p(J))\)最小的任务\(j\),将其放在最后执行,而后递归地对剩余任务应用上述步骤,这个算法称为Least-Cost-Last。
注意到此算法和我们之前的几个贪心算法的不同之处,之前的贪心策略可以按照同一个标准直接对\(n\)个任务排序,因此可以应用\(O(n\log n)\)的排序算法完成,但是此算法必须调度任务\(j\)后,才能根据新的\(p(J)\)值确定下一个被调度的任务,因此其时间复杂度为\(O(n^2)\)。此算法最优性的证明依然是通过最优解的下界来设计。
定义\(f_{max}^*(J)\)为任务集\(J\)的最优调度下的惩罚函数值,可以推导出两个下界:
\[ f_{max}^*(J)\ge \min_{j\in J}f_j(p(J)) \]
\[ f_{max}^*(J)\ge f_{max}^*(J-\{j\}) \]
第一个下界的来源是,必然有一个任务\(k\)最晚完成,它的惩罚函数参数恰好是\(p(J)\),从而有\(f_{max}^*(J)\ge f_k(p(J))\ge \min_{j\in J}f_j(p(J))\)。第二个下界来自于这样一个事实:如果从任务集中去掉一个任务,不会增加任何任务的完成时间,而惩罚函数是完成时间的非减函数,这意味着新的任务集的最优调度惩罚不增。
定理4.1.1:\(LCL\)算法是\(1||f_{max}\)的最优解。
证明:根据算法步骤,我们首先选取使得\(f_j(p(J))\)最小的\(j\)放在最后,其惩罚为\(\min_{j\in J}f_j(p(J))\),而后剩余任务集为\(J-\{j\}\),在这个任务集上的最优解为\(f_{max}^*(J-\{j\})\),因此\(LCL\)调度的惩罚值为\(\max\{\min_{j\in J}f_j(p(J)),f_{max}^*(J-\{j\})\}\),恰好是前面导出的最优解的下界。
扩展到\(1|prec|f_{max}\)
即使引入任务间依赖,Least-Cost-Last算法依然是最优的。在\(1|prec|f_{max}\)问题中,由于任务间存在依赖,因此稍微修改一下算法描述:我们将从当前未调度的无后继任务集\(L\)中,选择使\(f_j(p(J))\)最小的任务。这样的话,可以推导出一个新的下界:
\[ f_{max}^*(J)\ge \min_{j\in L}f_j(p(J)) \]
因此其最优性的证明完全类似上一节的证明,不再赘述。
另一种解法
\(Moore\)[33]给出过另一种\(1||f_{max}\)的解法,他的策略基于前面提到过的针对\(L_{max}\)的\(EDD\)算法。假设我们想知道是否存在一个调度能够使得\(f_{max}\le B\),可以给任务\(j\)分配一个截止时间\(d_j\),使得\(d_j=argmax_t\{f_j(t)\le B\}\),易知,如果每个任务都能在截止时间之前完成,那么该调度的\(f_{max}\le B\)等价于\(L_{max}\le 0\)(否则只要有一个任务晚于截止时间,惩罚就超过\(B\)),因此我们将\(f_{max}\)问题扩充了截止时间后,转化为了最小化最大延迟问题。因此一个想法是:对\(B\)进行二分查找,然后构造相应的截止时间,应用\(EDD\)来计算\(L_{max}\),从而逼近最优解。
\(1||\sum w_jU_j\)的动态规划解
现在我们考虑\(1||\sum w_jU_j\)问题,其优化目标是最小化已超时任务的总权重。此问题是弱\(NPC\)问题,即具有伪多项式时间算法,该算法复杂度为\(O(n\sum w_j)\),也就是说如果任务权重和能被一个\(n\)的多项式约束,那么算法可以在多项式时间内结束。实际上,如果所有权重都为1,那么问题\(1||\sum U_j\)可以直接用该算法解决,且时间复杂度退化为\(O(n^2)\)。而且,这个算法还可以用来导出一个此问题的\((1+\epsilon)\)-近似算法,该算法复杂度是关于\(n\)和\(1/\epsilon\)的多项式。
首先注意到,在这个问题中,任务集被划分为两种,一是在截止时间之前完成的任务,一是超时完成的任务。如果存在一个调度策略,使得一个任务集中所有任务都能在截止时间之前完成,则称这个任务集是可行的。显然,当一个任务集是可行的,那么其调度策略可以直接使用\(EDD\),因为\(EDD\)最小化最大延迟,在可行任务集下必然存在最大延迟不超过0的调度,从而使所有任务能按时完成。调度完成后,该可行任务集的完成时间就是所有任务的处理时间之和\(\sum p_j\)。
现在我们考虑\(1||\sum w_jU_j\)问题,它实际上是在寻找任务集\(\{1,2,...n\}\)中权重和最大的可行子集。首先将所有任务按照截止时间升序排列并编号,定义\(T_{wj}\)为任务集\(\{1,2,...j\}\)的权重和不小于\(w\)的可行子集族中,具有最小完成时间的子集的完成时间。如果不存在任何可行子集,则记为\(\infty\)。边界条件:记\(T_{w0}=\infty,\ T_{0j}=0\)。现在尝试计算\(T_{w,j+1}\),考虑\(\{1,2,...,j+1\}\)中权重和不小于\(w\)的完成时间最小的可行子集\(S\):
- 如果\(j+1\in S\),则我们按照\(EDD\)对\(S\)进行调度,\(j+1\)应放在最后执行,\(S-\{j+1\}\)的权重和不小于\(w-w_{j+1}\),且\(S-\{j+1\}\in \{1,2...j\}\),故\(T_{w,j+1}=T_{w-w_{j+1},j}+p_{j+1}\),当然如果计算出的\(T_{w,j+1}\)超过了截止时间\(d_{j+1}\),\(j+1\)就不可以放在集合\(S\)中。
- 如果\(j+1\notin S\),显然\(T_{w,j+1}=T_{w,j}\)。
状态方程总结如下:
\[ \begin{equation}T_{w,j+1}=\left\{\begin{array}\ min(T_{w,j},T_{w-w_{j+1},j}+p_{j+1})&if\ T_{w-w_{j+1},j}+p_{j+1}\le d_{j+1}\\T_{w,j}&otherwise\end{array}\right.\end{equation} \]
显然,不存在权重超过\(\sum w_j\)的可行子集,因此一旦\(w\)超过该值就可以终止程序。覆盖所有\(w\)值的时间复杂度为\(O(n\sum w_j)\),一旦计算完所有的\(T_{wj}\),我们只需要找到使得\(T_{wn}<\infty\)的最大的\(w\)对应的可行子集,对该子集进行\(EDD\)调度,而后将其余任务最后调度,这就是\(1||\sum w_jU_j\)的最优调度策略。
\(P||C_{max}\)的动态规划解
我们前面已经介绍过一些\(P||C_{max}\)的近似算法,其中最长任务优先算法\(LPT\)能够达到\(4/3\)的近似比,同时我们也知道这个问题是\(NP\)难的。这一节我们假设所有任务的处理时间都在一个有限集中选择,则问题可以在多项式时间内解决。
定理4.2.1:\(P||C_{max}\)问题中,任务数\(n\),机器数为\(m\),\(p_j\)是任务\(j\)的处理时间,如果满足\(\{p_1,p_2,...p_j\}\subseteq S\)且\(|S|=s\le \infin\),则存在一个最优调度算法,其时间复杂度不超过\(n^{O(s)}\)。
证明:同样适用动态规划的思路,设\(S=\{z_1,z_2,...,z_s\}\),注意到在上面的约束下,一台机器上运行的任务集可以用一个\(s\)维向量来表示:\(v=(v_1,v_2,...,v_s)\),其中\(v_k\)表示在此机器上执行的处理时间为\(z_k\)的任务的数量。同时由于共有\(n\)个任务,意味着这样的\(s\)维向量共有\(n^s\)个。设\(T\)是目标完成时间,也就是说所有机器需要在\(T\)时刻之前完成所有分配的任务。设\(V\)是所有处理时间小于\(T\)的(即\(\sum v_iz_i\le T\))的向量集合。在满足目标完成时间\(T\)的调度策略中,每个机器必然都从\(V\)中选择一个任务集来执行。定义\(M(x_1,...,x_s)\)是完成任务集\((x_1,x_2,...x_s)\)的最小机器数量。容易看出下面的等式成立:
\[ M(x_1,...,x_s)=1+\min_{v\in V}M(x_1-v_1,...,x_s-v_s). \]
实际上就是从中去除一个机器承担的任务后,递归地计算剩余任务所需的最小机器数,最终计算最小值。完整的执行这个动态规划算法需要首先遍历\(n^s\)个向量组合,每个向量需要遍历\(n^s\)次剩余任务,因此时间复杂度为\(O(n^{2s})\)。
还需要考虑的是\(T\)的选择,可以在所有可能的任务向量对应的处理时间中选择\(T\),应用算法确定最小机器数目,如果数目大于机器数\(m\)则减小\(T\),否则增加\(T\),据此可以应用二分查找。最终得到使得\(M=m\)的最小的\(T\)对应的调度策略就是最优调度。
图与线性规划
网络图算法和线性规划算法是组合优化问题的核心主题。能够用来解决许多难题,当然也能用于调度算法的设计。在这一节,我们考虑应用二分图匹配和线性规划算法来解决调度问题。
二分图匹配
一个二分图包含两个点集\(A\)和\(B\),以及边集\(E\subseteq A\times B\),它的一个匹配\(M\)定义为\(E\)的子集,且任何一个顶点都至多在\(M\)中的一条边上。我们可以考虑将任务和机器进行匹配,这样的话根据上面的定义,每个任务最多只会被分配给一台机器,同时一台机器最多只会被分配一个任务。如果\(|A|\le |B|\),则如果\(A\)中所有结点都在匹配\(M\)的一条边上,则称这个匹配是完美匹配。当然,也可以为每条边赋权重,而后定义匹配的权重为其中所有边的权重之和。一个很重要的事实是,二分图的最小完美匹配是有多项式时间算法的。
\(R||\sum C_j\)问题
在不相关并行环境下优化调度的平均完成时间,似乎是一个挺难的问题,本节将给出一个多项式时间算法。对任何调度,设\(\kappa_{ik}\)为在机器\(i\)上倒数第\(k\)个运行的任务,\(l_i\)为机器\(i\)上运行的任务数量。注意到,一个任务的完成时间等于在该任务之前(包括自身)运行的任务处理时间之和,我们有:
\[ \sum_j C_j=\sum_{i=1}^m\sum_{k=1}^{l_i}C_{\kappa_{ik}}=\sum_{i=1}^m\sum_{k=1}^{l_i}\sum_{x=k}^{l_i}p_{i,\kappa_{ix}}=\sum_{i=1}^m\sum_{k=1}^{l_i}kp_{i,\kappa_{ik}}. \]
其中\(p_{i,j}\)为机器\(i\)上运行任务\(j\)需要的处理时间,实际上最后一个等号的意思是:倒数第\(k\)个任务对机器\(i\)上的所有任务完成时间和的贡献是其处理时间的\(k\)倍(很容易理解的)。
现在我们定义一个二分图\(G=(V,E)\),其中\(V=A\cup B\),\(A\)包含\(n\)个顶点,对应于\(n\)个任务\(v_j\),\(B\)包含\(nm\)个顶点\(w_{ik}\),代表机器\(i\)上倒数第\(k\)个处理的任务,显然\(i=1...m,k=1...n\)。边集\(E\)包含所有\((v_j,w_{ik})\),为每条边赋予权重:\((v_j,w_{ik})\rightarrow kp_{ij}\)。下面证明在这个二分图上寻找到的最小完美匹配对应于最优调度。
证明:首先,每一个有效的调度策略都对应一个完美匹配(这是显然的),其次,并不是每个完美匹配都对应一个有效的调度策略,因为一个任务可能被指定为倒数第\(k\)个运行但是该机器上并没有满\(k\)个任务。但显然这样的匹配不是最小匹配,因为我们可以将\(k\)减小,以获得更小的权重。因此最小完美匹配一定对应一个有效的调度策略,且根据之前的分析,该匹配的权重和等于所有任务的完成时间总和,从而该策略是最优的。
关于这个问题的简化版\(P||\sum C_j\),也可以通过上面的分析看出,每个机器上倒数第\(k\)个完成的任务\(j\)导致时间和增加了\(kp_j\),因此基本思路就是长任务尽量晚地在机器上执行,也就是先将前\(m\)长的任务分配给各机器处理队列的尾部,重复上述步骤。这和最短任务优先\(SPT\)算法的结果一致,因此\(SPT\)算法不仅是\(1||\sum C_j\)的最优解,还是\(P||\sum C_j\)的最优解。
\(O|pmtn|C_{max}\)问题
第二个使用二分图匹配来设计算法的是\(O|pmtn|C_{max}\)问题,即开放车间下的抢占式调度优化最终完成时间。我们在之前使用\(LS\)算法解决过\(O||C_{max}\)问题,其中提到两个下界:\(\Pi_{max}\)是单个机器的最大负载时间(每个任务在单个机器上需求的处理时间之和),\(P_{max}\)是单个任务的最大处理时长。这两者仍然是抢占式调度下该问题的下界。实际上,允许抢占式调度的情况下,使用二分图匹配算法能够达到最优策略,其完成时间等于\(\max(P_{max},\Pi_{max})\)。
我们考虑对此问题的一个调度的任意时间点,在这个时间点,每个机器至多在处理一个任务,也就是说,在每个时间点,调度策略都指定了一个任务到机器的匹配。我们的目的是找到任意时间点的一个符合最优调度的匹配,并按照这个匹配来执行任务。
我们称总剩余处理时长等于\(P_{max}\)的任务为\(j_{max}\),称机器负载时间达到\(\Pi_{max}\)的机器为\(m_{max}\),也称它们是紧迫的,注意随着时间的推进,这两个值是变化的(有的任务可能不在匹配中,因为没执行)。如果能够让某时间点的匹配包含\(j_{max}\)和\(m_{max}\),那么在这个匹配下执行\(t\)时间,我们就可以将这个调度的理论下界\(\max(P_{max},\Pi_{max})\)降低\(t\)。如果任意时间点的匹配均满足这一条件,那么最终能够将这个下界降低到零,这说明此时最大负载的机器已经处理完毕,所有任务都执行完毕,而处理时间恰好等于\(\max(P_{max},\Pi_{max})\),这就是最优调度策略。同时这个匹配还必须满足所有边的剩余执行时间为正(这样才能继续执行),满足这些条件的匹配称为\(decrementing\ set\),它的存在性证明较难,超出本文范畴,有兴趣的读者可以去读Lawler和Labetoulle的原论文[28]。
下面我们构造一个二分匹配问题来寻找它,这里比上一节的图简单。点集只需包含所有的任务\(i\)和机器\(j\),同时包含每条边满足如下条件:连接一个机器\(i\)和一个任务\(j\),且任务\(j\)在机器\(i\)上剩余所需的处理时间大于零。显然,根据上一段的分析我们需要这些边中包含进\(j_{max}\)和\(m_{max}\),应用一些传统匹配算法很容易保证这一点。
现在我们找到了一个时间下的满足条件的匹配,只需按照这一匹配执行\(t\)时间,直到其中某个机器上的任务剩余时间等于零,或者某个机器剩余处理时间达到零,或者有新的任务或机器变紧迫(这是因为一个匹配可能不会包含所有任务和机器,导致其剩余处理时间没变),才寻找新的匹配。因此需要在许多时间点运行匹配算法,这会不会导致最终时间复杂度超过多项式时间呢?答案是不会。
证明:我们只会在特定条件(见上一段)满足时才寻找匹配,每个任务-机器对只会完成一次,因此最多需要运行\(nm\)次匹配,同时每个任务或机器只会变紧迫一次,因此最多\(n+m\)次。两者加起来不超过多项式时间。
为缩短文章长度,后续内容见调度算法(二)。