漫谈算法(一)如何证明贪心算法是最优 using exchange argument
刘子韬
这里主要是介绍一种证明贪心算法是最优的一种方法:Exchange Argument (不知道应该怎么翻译到中文,交换参数?感觉听起来挺别扭的,不像是一个方法的名字~o(╯□╰)o)
Exchange Argument的主要的思想也就是 先假设 存在一个最优的算法和我们的贪心算法最接近,然后通过交换两个算法里的一个步骤(或元素),得到一个新的最优的算法,同时这个算法比前一个最优算法更接近于我们的贪心算法,从而得到矛盾,原命题成立。
下面来看一个更为formal的解释:
步骤:
Step0: 给出贪心算法A的描述
Step1: 假设O是和A最相似(假设O和A的前k个步骤都相同,第k+1个开始不同,通常这个临界的元素最重要)的最优算法
Step2: [Key] 修改算法O(用Exchange Argument,交换A和O中的一个元素),得到新的算法O’
Step3: 证明O’ 是feasible的,也就是O’是对的
Step4: 证明O’至少和O一样,即O’也是最优的
Step5: 得到矛盾,因为O’ 比O 更和A 相似。
证毕。
当然上面的步骤还有一个变种,如下:
Step0: 给出贪心算法A的描述
Step1: 假设O是一个最优算法(随便选,arbitrary)
Step2: 找出O和A中的一个不同。(当然这里面的不同可以是一个元素在O不再A,或者是一个pair的顺序在A的和在O的不一样。这个要根据具体题目)
Step3:Exchange这个不同的东西,然后argue现在得到的算法O 不必O差。
Step4: Argue 这样的不同一共有Polynomial个,然后我exchange Polynomial次就可以消除所有的不同,同时保证了算法的质量不比O差。这也就是说A 是as good as 一个O的。因为O是arbitrary选的,所以A是optimal的。
证毕
下面给几个例子:
例 Maximum Cardinality Disjoint Interval Problem
问题描述:给一些时间片段集合T={(a1,b1)(a2,b2),。。。,(an,bn)},找出一个元素个数最多的子集S,子集中的每个元素的时间片段没有交叉。
Greedy Algorithm: 每次都选所有interval 中bi最小的那个,把(ai,bi)加入S,然后把(ai,bi)在T中删除,同时把T中所有和(ai,bi)有交叉的interval删除,然后再在T中找最小的bj,循环上面的操作,直到没有可以在添加的。
证明上面说的Greedy Algorithm是最优的。
下面就用第一个证明的步骤来证。
我们的Greedy Algorithm记为A,假设A不是最优的,那么就一定存在一个O,O是和A最相近的一个最优的算法,最相近是指和O和A的前K-1个选择都相同,第K个是不同的。
假设对于A,A第k个选择的是(ai,bi);而O第K个选择的是(aj,bj)。从A的定义我们可以直到,bi<=bj。
现在我们构造一个O,O = O-(aj,bj)+(ai,bi)。
1)很显然,O是这个问题的一个解,也就是说O中的intervals没有重叠的。
在O中,(ai,bi)前的intervals和A中的一样,所以前一部分没有重叠。在(ai,bi)后的intervals和O中的一样,所以也没有重叠,同时bi<=bj,所以(ai,bi)也不会和它相邻的重叠,所以O中的所有intervals都没有重叠。
2)O是一个最优解,因为他的intervals的个数和O一样。
综上,我们找到了一个最优解O,它和A具有的共同的intervals有K个,这和我们前提假设最多有k-1个相矛盾,所以,A是最优的。证毕。
例 Sche易做图ng with Deadline
问题描述:有一系列的tasks{a1,a2,。。。,an},每个task需要在cpu上跑{p1,p2,。。。,pn}个units的时间,cpu是非抢占式的,也就是task一旦获得cpu,就运行直到他完成,{c1,c2,。。。,cn}是每个task的完成时间,我们现在要minimize的就是 (c1+c2+。。。+cn)/n。所有的task一起release。
Greedy Algorithm:每次选运行时间最小的那个task运行。
证明上面说的Greedy Algorithm是最优的。
同样还是用第一个证明的步骤来证。
假设A不是最优的,那么一定存在一个最优的O,和A最接近,他们选择的前k-1个task是相同的。
如下:
No. 1 2 3 。。。 k 。。。。。。。。。
A 。。。。。。。。。。。 ai 。。aj 。。。。。
O 。。。。。。。。。。。 aj 。。。。ai 。。。
根据A的定义,我们知道,pi <= pj的。
现在我们就来构造O,当然,O就是把O中的ai和aj两个元素交换一下,即
No. 1 2 3 。。。 k 。。。。。t。。。
A 。。。。。。。。。。。 ai 。。aj 。。。。。
O 。。。。。。。。。。。 aj 。。。。ai 。。。
O 。。。。。。。。。。。 ai 。。。。aj 。。。
对于整个的完成时间,我们可以给出计算公式的,假设b1,b2,。。。,bn是一个调度序列(bi是task的index,也就是{bi}是所有task的index的一个permutation),即task执行顺序是a_{bi},a_{b2}, ..., a_{bn}。举个例子如果只有5个task,a1,a2,a3,a4,a5。{bi} = {2,1, 3, 5, 4},那也就是task的执行序列为 a2,a1,a3,a5,a4。(博客里面不能打数学公式就是不爽啊~)
还是用上面的5个例子算了,对于a2,a1,a3,a5,a4,我们知道总时间其实是5个a2的时间,即p2,4个a1的时间,即p1,依次类推,所以总时间是5*p2+4*p1+3*p3+2*p5+p4。
好,有了上面的计算总时间的概念,下面就来分别计算一下O和O的总时间,其实我们需要比较它们谁大谁小,通常只要比较一下他们的差就行了,即Time(O)-Time(O)。同时很显然,他们的差只是由aj和ai引起的,其他的并没有改变。我们知道在O中,aj是第k个选的,ai是第t个选的,而在O中,ai是第k个选的,aj是第t个选的,所以 Time(O)-Time(O) = (n-k+1)*pj + (n-t+1)*pi - (n-k+1)*pi - (n-t+1)*pj = k*(pj-pi) - t*(pj-pi) = (pi-pj)*(k-t)>=0,即我们得到Time(O) >= Time(O),这也就是说我们找到了一个O,他是最优的,同时O和A有K个common element,所以得出矛盾。证毕。
例 Kruskal Algorithm for Minimum Spanning Tree(最小生成树)
问题描述:对于边集合E,先按每个边的cost排序,从小到大,然后按如下方法构造一个MST,每次选cost最小的那个边,添加进我们的MST,如果一个边使我们现有的MST出现环路,则把这条边舍弃,然后重复上面的操作。(感觉现在表达越来越搓了,没看明白怎么回事的童鞋看这里吧 http://en.易做图.org/wiki/Kruskals_algorithm)
下面我们就用第二种证明方法来证明Kruskal Algorithm算法是最优的。
假设我们的graph是G(V,E),有一个optimal的算法O,它生成的MST是T1,Kruskal生成的树是T2,这样,因为T2!=T1(如果相等我们的Kruskal就最优了,就不用证了),所以至少有一条边e,e在T1中,同时e不在T2中。这个与众不同的e就是我们的切入点。可以想象,如果我们把e在T1中去掉,T1就变成了两部分,即Ta和Tb,我们知道 {Ta中的点}V{Tb中的点} = V(就是Ta中的点并上Tb中的点就是我们G中的点集V),所以在T2中,一定有一条边f(f!=e),f的一个点在{Ta中的点},f的另一个点在{Tb中的点}。根据我们的Greedy算法Kruskal,我们没有选e,是因为e在我们的图中造成了回路。在进一步想,有回路是因为我们先选了f,这就说明cost(f) <= cost(e)。 (恩,这就是我们的重点啊,笑一个O(∩_∩)O~)
下面的也就很显然了,我们考虑构造一棵新的树,T3,对于T3 = E(T1) - e + f,也就是T3中的边是T1中的所有边除了e,同时加上边f。
1)很显然,我们知道T3是一棵spanning tree,f连通了由去掉e而分隔的两部分。
2)很显然,T3的cost是<= T1的cost的。 因为cost(f) <= cost(e),且其他不变。即T3是MST。
所以同个上面的构造,我们就构造出了一棵树,这个树是MST,同时它比T1更接近于T2。(多了一条common的边)
我们知道T1和T2对多有n条边是不同的,通过上面的步骤,我们可以经过n次变换,得到T2,同时cost <=T1,所以,T2是MST。证毕。
补充:软件开发 , C语言 ,