引言
在学习完分治和动态规划之后,我们来学习贪心算法。解决问题的观察思路和解决方法的选择可如下图所示。
可分可以使用分治思想,如果是最优化问题,并且可以多步决策、有最优子结构,则可以使用动态规划,再进一步,如果还具有贪心选择的性质,则可以使用贪心算法。
贪心算法概念
贪心算法和动态规划很像,贪心算法主要是在动态规划上多了贪心选择性质,贪心算法是在多步决策每一步都要直接最优,而不是通过动态规划那样枚举,最后这些局部最优解组成了全局最优。
排课问题
我们利用排课问题来具体阐述贪心算法与动态规划的差别。
问题描述&分析
一间教室,多门课程要在不同时段使用这间教室,给定第i节课
A
i
A_i
Ai从
S
i
S_i
Si时间开始上,到
F
i
F_i
Fi时间结束,上第i门课的学生数量是
W
i
W_i
Wi,如何排课能让尽可能多的学生上课?
因为课程时间段可能相交产生冲突,所以排课必须是时间不相交的(如上图两个示例solution),且要使得上课学生数量最大。
解决方法
1.动态规划
首先,最简单的情况课程数n=1的时候,直接安排上唯一的课就行了,n=2的时候就会产生时间冲不冲突的问题,导致安排与否,不冲突则都安排,冲突选W大的。当n再多的时候,问题就复杂了,如何最优?
我们来按照多步决策分析一下,我们选择从后往前,看一下安不安排 A 9 A_9 A9:
- 安排:新的可选时间段缩短到 A 9 A_9 A9上课前,递归调用剩下的所有在时间段内课程
- 不安排:时间段不变,去除
A
9
A_9
A9,递归调用
A
1
.
.
.
A
8
A_1...A_8
A1...A8
剩下的决策过程都是一样的,但是这里有一个小问题,如何快速找出所有在 A 9 A_9 A9上课前的所有课程?如果在选定 A 9 A_9 A9上了之后,再循环遍历挑出来,那每一次决策都要遍历就太麻烦来。所以预先对所有课程按照下课时间排序,这样我们很容易找到下课在 A 9 A_9 A9上课前的课程。
这样我们就可以写出动态规划的转移方程式了。
设对课程
A
1
,
.
.
.
,
A
i
A_1,...,A_i
A1,...,Ai的最优选择结果是OPT(i),在第i节课上课前的最近下课课程编号是pre(i)(例如pre(9)=7 )。方程式如下:
O
P
T
(
i
)
=
{
O
P
T
(
p
r
e
(
i
)
)
+
W
i
;
O
P
T
(
i
−
1
)
;
OPT(i) = \begin{cases} OPT(pre(i)) +W_i ; \\ OPT(i-1) ; \end{cases}
OPT(i)={OPT(pre(i))+Wi;OPT(i−1);
对于每步决策有两个选项(选/不选),然后对应每个选项再递归调用。
伪代码如下:
2.贪心算法
上面的动态规划方法已经解决了这个问题,我们再看看在DP的基础上能否找出贪心规则/性质进一步使用贪心算法。我们前面动态规划的思路是对每门课进行多步决策(选不选这门课),实际上最终的解可以表示成X=[1,0,0,1,…,1]的形式,其中1表示选,0表示不选。
如果我们从另一个角度思考,只考虑那些要选的课,那么决策就转换成了,我每次要选哪一门课。这有9个选项,对于每一个选项,我在对除去该课和与该课冲突的课程的子集中继续选择。
这样转移方程式就变成了:
O
P
T
(
S
)
=
max
A
i
∈
S
{
O
P
T
(
S
′
)
+
W
i
}
OPT(S) = \max_{A_i\in S}\{OPT(S')+W_i\}
OPT(S)=Ai∈Smax{OPT(S′)+Wi}
S是当前可选课程集合,S’是去除
A
i
A_i
Ai和与该课程时间冲突的课程后的集合。每一轮去枚举求当前可选集合里的最大解。
伪代码如下:时间复杂度
O
(
2
n
)
O(2^n)
O(2n)
这相当于把所有的可能组合都枚举了出来,而对于集合元素数n的可能组合有
2
n
2^n
2n种,因此时间复杂度为
O
(
2
n
)
O(2^n)
O(2n),这与平常动态规划的时间复杂度有所不同,主要是因为没有存储动态规划数组计算的结果,例如我先选1,再选2和先选2再选1应该是一样的效果,这里出现了冗余计算。
为了引入贪心算法,下面我们考察一个简单的特例:
假设每门课均只有一个人上,上面的例子转换后如下所示:
在贪心中直接选择最早下课的那门
A
1
A_1
A1,因为它肯定会出现在最优解中,不需要枚举其他的情况。
下面是简单的证明:
- 反证法
假设存在一个最优解不包括 A 1 A_1 A1 ,例如该最优解是 A 2 , A 4 , A 7 , A 8 A_2,A_4,A_7,A_8 A2,A4,A7,A8,显然我们可以用 A 1 A_1 A1替换掉 A 2 A_2 A2 ,构造新的最优解 A 1 , A 4 , A 7 , A 8 A_1,A_4,A_7,A_8 A1,A4,A7,A8,这里人数未发生变化(因为 W i = 1 Wi=1 Wi=1),新的最优解中就有了 A 1 A_1 A1。
这样就相当于第一次原本要考虑1-9,枚举一遍,现在直接不需要计算2-9,直接选中了1,瞬间减少了计算。而在第1门课被选定之后,在剩下的课程中,我们依然每次都找最早下课的,但要求选的课不能和已选的冲突(上课时间要晚于最近选择的下课时间)。这样,我们相当于只用做一次循环遍历就可以找出最优解。
解释:首先按照下课时间对课程排序,循环遍历,发现B最早下课,将B安排上,然后到C,C虽然是新最早下课,但与B冲突,不安排,A也同理,一直到E,安排上,D,F,G与E冲突,不安排,H可行,安排上。
伪代码如下所示:时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
原本的排课问题只能通过传统的动态规划方法快速解决,无法直接使用贪心算法,在改变了一些条件之后(每个课的学生数量W相同),我们就可以用贪心解决,因为每一次只用找最早下课的即可(局部最优)。贪心算法相当于解决了原问题的一个简化版本,这时候贪心的算法优点代码量少,且贪心不需要额外的存储空间(动态规划数组)。
总结
贪心算法与动态规划之间的相似点:
- 都针对最优化问题,因此看到最优化问题,首先想到动态规划与贪心
- 最优子结构,动态规划与贪心都要寻求最优子结构,即子问题的最优解可用于组合成原问题的最优解
- 所有的贪心算法都可以找到一个笨拙动态规划的版本
不同点:
- 动态规划需在每一步决策时枚举所有可能的选项,只有子问题解决后,决策才能做出判断。例如 A 9 A_9 A9选与不选先要依赖前面最优选择情况,文中例子是选的情况询问 A 1 . . . A 7 A_1...A_7 A1...A7 ,不选的情况询问 A 1 . . . A 8 A_1...A_8 A1...A8
- 贪心算法则不必枚举所有可能情况,它在当前决策下就直接选定了局部最优,而不考虑子问题的选择。减少冗余计算,但它也需要条件(在排课问题中就是每门课学生人数都要相等),这也是贪心选择的性质/规则
如何设计一个贪心算法?
- 先设计一个动态规划算法,再看看能不能greedy-selection不枚举
- 试错,将解的生成过程描述为一系列的选择,并尝试不同的greedy-selection规则(排课问题中,选开课时间最早的课/选时间上课最短的课/选与其他冲突最少的课/选最晚上课的课/选最早下课的课)。