// 大牛可以自动忽略以下内容 ==========================================================================
这里不采用严格数学方法证明,只是给出一些有助于记忆原理的想法。
先引入一个有关的问题。
如果一个整数N的约数只含有2,3, 5中若干的话,我们把N称作ugly_number。譬如说,2,4,12,15,60都是ugly_number。
现在的问题是,要求你按大小顺序输出前K个ugly_number。
这个问题的一个可行解法是:
(1). 建立一个最小堆h,将2丢进去
(2). lastOutput = -1
(3). m = 取出最小堆里的最小元素
(4). 如果 lastOutput 不等于 m,那么输出m,并且令lastOutput = m;否则回到(3)
(5). 将m * 2, m * 3,m * 5丢进最小堆里
(6). 回到(3)
这个算法为什么是正确的?
为了方便叙述,如果一个数a是ugly_number,那么a * 2,a * 3,a * 5 被称作是 a 的 儿子,a是它们的父亲
假设我们把所有ugly_number按照递增顺序列出来,那么,大数肯定是由前面的小数派生出来,或者说,第n个数肯定是由前面的某个数k派生出来的(k*2, k*3, k*5中的一个)。
所以,如果我们手上得到了前r个ugly_number数的时候,我们把这r个数的儿子全部放进一个容器里,找出容器里最小的那个数,那么,那个数必定就是第r+1个ugly_number,因为第r+1个数肯定是前面r个数里的某个数的儿子
回到标题:为什么Dijkstra是正确的?
其实原理跟上面那个问题的解答一样。对于一个含有N个点的图,起点为v0,我们可以按照长度递增列出N条最短路径:
p1, p2, p3, ....pn (此序列称作序列#)
其中,p1={v0}是肯定的。
由于最短路径具有最优子结构,所以,对于序列里的某一个最短路径p[r],如果去掉它的最后一个点,必定可以得到它前面的某个p[m],m < r。
因此,如果我们手上有了前面的p1,p2,p3...p[r],然后我们算出所有这些路径派生出来的“儿子”最短路径”,其中那个最短的那个肯定就是p[r+1]。
慢着,这里漏了一个关键点,刚才我们说,去掉p[r]的最后一个点得到路径p[m],p[m]必定位于p[r]的前面,也就是说,p[m]必定小于p[r]。这个命题的正确性至关重要,因为只有当这个命题成立的时候,我们才能保证“长的最短路径是由短的最短路径派生出来的”。
那么,这个命题是不是一定成立?
其实不一定的,因为图的边可以具有负数的权值,如果一个图含有负数权值的边,我们就不能保证上面命题的成立了。
所以,我们平时会说,Dijkstra算法,只能应用于没有负数权值的图,就是这个原因了。
总结一下就是,Dijkstra算法的正确性需要两个条件:
1. 最短路径问题具有最优子结构 => 这个条件告诉我们:一个最短路径可以由其他某个最短路径推算(派生)出来。
2. 图不包含负数权值的边 => 这个条件告诉我们: 一个长的最短路径肯定是由一个比它短的最短路径派生出来的。也就是说,最短路径的发现过程可以存在单调性,先发现短的,然后利用短的来发现长的。又或者说,如果两个最短路径具有派生关系的话,我们就认为有一条边从“父亲”指向“儿子”。按照长度对最短路径序列排序后,我们会看到,所有的边都是从左指向右的(类似于拓扑排序后的效果),所以我们可以先算左边的,然后算右边的。这个技巧,其实就是动态规划里的那一套!
说到DP,其实无论是图的最短路径还是DP,归根到底,都是“具有最优子结构”的问题,这一类问题,其实就是计算若干个最优解,而计算它们的策略,是可以有很多种的,千万不要绑住自己的想象力,只要能正确地算出来,用什么方法都ok。
完了。