斯坦福 Algorithms: Design and Analysis 2 第一周笔记
来自斯坦福网站的Algorithms: Design and Analysis,与目前coursera上的版本内容没有变化,不过时间安排略有不同。
1. Introduction
这里介绍了两个比较经典的算法问题作为引子,在这么课中将会学会解决这两个问题的算法。
1.1 Internet routing
因特网与图这个数据结构非常有关系。因特网本身可以看成一个图,结点是每个主机以及每个路由器,边是每个节点之间相连的网线或者无线网。而网络中还有其它可以看成图的,比如网页图。每个网页看成一个节点,网页间的超链接看作是边。还有社交网络等等。
网络路由问题:
假设从斯坦福的网关路由器发送数据到康奈尔,需要选择一条最短的传播路径,假设传播的跳数最少就是最短。那么这就是图中的一个最短路径问题。算法1中讲到的dijkstra算法可以解决这个问题,但是这个算法需要每个路由器知道全局信息,但这是不可能的。我们需要一个只知道local信息(与其相连的结点信息)就能得到最短路径的算法,这就是贝尔曼福特算法。
1.2 sequence alignment
序列对齐问题:假设有两个字符串序列,需要判断它们相似的程度。相似的定义是,两个字符串对齐之后完全相同的字符所占比重。需要插入空格和对齐后的字符不相等都算是penalty。两个字符串对齐之后的penalty越小就说明它们越相似。这个penalty叫做NW score。
于是我们需要一个算法来计算两个字符串的对齐。如果有两个长度为500的字符串,使用暴力做法尝试所有可能的对齐方式,这个复杂度是,永远不可能实现。但是使用动态规划就可以很快解决这个问题。
2. Greedy Algorithms
2.1 Introduction
这部分介绍了贪心算法。贪心算法是四种算法的设计思想之一,这四个思想为:
- 分治法
- 随机算法
- 贪心算法
- 动态规划
贪心算法没有一个严格的定义,大致的意思是说,算法在当前情形下选择目前看来最好的选择,期望这么做到最后得到的结果也是最好的。一个例子就是dijkstra算法。
和分治法有很多不同点:
- 分治法对每个问题都需要有具体设计,贪心法是个很容易用到不同问题的方式。
- 分治法时间复杂度分析很困难,贪心法一般很简单。
- 分治法正确性比较容易证明,贪心法一般很难证明。
DANGER:大部分贪心算法都不正确。
比如虽然dijkstra算法是对的,但在有负权重的情况下继续贪心就会导致错误:
正确性证明的几种方式:
- Induction(貌似包括演绎法与归纳法):比如dijkstra算法的证明。
- exchange argument:貌似指的是反证法,但感觉不完全是。意思就是假设一个最优解,然后与算法得到的解交换一些元素,得到矛盾就是反证法。也有可能会直接推导出算法当前解同样也是最优的。
- 其它任何方法。
2.1.1 Optional Caching
缓存的替换机制就是贪心法的一个应用。
缓存的替换不再介绍,但是缓存的替换有一个最优的指导原则,那就是去掉当前缓存中在未来被再次访问时距离当前时间最远的页。最优指的是类似缓存的访问miss最少。
不过算法不会预知未来,这个原则的意义在于:1.可以作为真实算法的指导。比如可以用这个原则知道利用data的locality性质LRU算法会效果更好一些。 2. 可以做一些用来测试的benchmark。
2.2 A SCHEDULING APPLICATION
一个调度问题定义为:有共享的资源(如一个处理器),有很多job需要做(如处理线程)。问题是该如何给这些job进行调度排序。可以假设每个job有一个表示优先度的weight,以及处理时间l。每个任务有个完成时间,表示当前任务以及之前所有任务的时间和。调度任务的目标函数是让完成时间的加权和最小,即。
比如:三个任务,,它们顺序执行后完成时间的加权和为15。
算法设计:当每个人物时间相同时,显然让权重大的先执行。而当所有任务权重相同时,让时间少的先执行。但是一般情况下,我们同样需要一个“分数”来给任务进行排序。定义分数为然后按分数降序排列任务即可。
正确性证明: 我们需要证明的是,按分数进行降序排列后的顺序即最优。每个任务按照分数降序用[1, 2, 3, 4, … , n]表示。首先假设这个分数是无重复的。用反证法(argument exchange)可以很容易证明。
假设贪心算法得到的结果为,其最优解为。而贪心法的结果的执行顺序就是[1,2,3,4,…,n],假设这两者不相同,那么最优解中必然存在两个连续的任务i > j。
然后发现交换最优解中的i和j的执行顺序后,得到的目标结果完成时间的加权和更小了(可以自己算算看)。也就是说假设不成立,也就是证明了最优解必须是贪心得到的结果。
这只解决了分数不重复的情况。如果存在重复时算法正确性依然可以证明。
正确性证明:假设为贪心算法,是任意的其它算法。
其它算法中必然也存在逆序对。我们将连续的逆序对进行交换,发现交换后的结果只会比交换前更好或一样好。也就是说按照贪心算法得到的结果[1,2,3,…,n]跟任意算法相比都是一样好或更好。也就证明了它必然是最优解。
3. Minimum Spanning Trees
最小生成树的非正式形式的目标是,将一堆点用最小的代价连起来。
最小生成树有两个很快的算法:Prim算法和Kruskal算法。前者与dijkstra很类似。
问题的正式定义如下:
最小生成树T有两个特点:1.T中无环。2. T是一个联通图。
在本章中还有两个不影响结果的假设:1.原图G是联通的。 2. 每条边的cost不重复。(为了证明的简化)。
3.1 Prim’s MST Algorithm
和dijkstra算法的区别在于每次假如结点考虑的是结点到集合的最短距离,而dijkstra算法是到源点s的最短距离。
3.2 Correctness Proof
第一步:证明Prim算法得到的是生成树。
首先定义图的割(cut)。一个割指的是将图G中的点分成非空两个部分的一个分割。
引理:Empty Cut Lemma,是说一个图G如果存在一个割(A,B),A和B之间没有边相连,那么这个图G是个非联通图。
还有两个事实:
证明过程如下:
第二步:证明prim算法得到的生成树是最小生成树。
利用一个引理,割的性质。
利用割的性质,我们知道Prim算法每一步加入的边都是最小生成树的边,而上一步已经证明的Prim算法最后的结果是一个生成树。那么就证明了这个树是一个最小生成树。
The Cut Property的证明(假设每个边的cost唯一):
利用反证法:如果割(A,B)间有一个最小cost的边e不属于MST T*。通过e与T中另外的边进行交换让T的总cost更小来得到矛盾。不过问题在于如何确定T*中与e交换的边。
事实上,我们不能随意指定一条边f与e进行交换,这样可能会让交换后的结果无法构成生成树。但是确实存在一个边可以完成与e交换的调剂。
已知T*是一个生成树,因此假如边e之后必然构成一个环。由前面的引理知道,这个环中必然有另一个边e’也跨越这个割(A,B)。因此可以选择将e与这个边e’进行交换。就能够完成The Cut Property的证明。(需要说服自己e与e‘交换之后得到的一定是一个生成树emmm,好的我说服自己了。)
3.3 Fast Implementation
用naive的方法,同样是需要进行n-1次循环,每次循环都要遍历每条边。因此复杂度会是。
因为算法与dijkstra算法如此相似,自然能够想到用堆来加速算法。
与dijkstra相比有两个不变的地方:1.堆中的元素依然是V-X集合中的点。2.对于集合V-X中的每个点v,key[v]是最短的边(u,v)的cost,其中。(这里应该变了吧,dijkstra算法里的key[v]是v到源点s的最短的cost吧???)
初始化堆的复杂度是。
算法流程如下:
用堆加速后的Prim算法,需要在初始化时进行n-1次插入,一共进行n-1次输出最小操作。每条边都可能会触发一次堆的delete与insert操作。一共有次的堆操作,因此算法的复杂度是。