汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?
分析
如果是初次接触类似的问题,乍看之下肯定会感觉无从下手。
要把64个圆盘从a柱子移动到c柱子上,第一步应该怎么做?虽然可以肯定,第一步唯一的选择是移动a最上面的那个圆盘,但是应该将其移到b还是c呢?很难确定。因为接下来的第二步、第三步……直到最后一步,看起来都是很难确定的。能立即确定的是最后一步:最后一步的盘子肯定也是a最上面那个圆盘,并且是由a或b移动到c——此前已经将63个圆盘移动到了c上。
也许你会说,管他呢,先随便试着移动一下好了。如果你这么做,你会发现,接下来你会面临越来越多类似的选择,对每一个选择都“试”一下的话,你会偏离正确的道路越来越远,直到你发现你接下来无法进行为止。
如果将这个问题的盘子数量减为10个或更少,就不会有太大的问题了。但盘子数量为64的话,你一共需要移动约1800亿亿步(18,446,744,073,709,551,615),才能最终完成整个过程。这是一个天文数字,没有人能够在有生之年通过手动的方式来完成它。即使借助于计算机,假设计算机每秒能够移动100万步,那么约需要18万亿秒,即58万年。将计算机的速度再提高1000倍,即每秒10亿步,也需要584年才能够完成。注:在我的笔记本电脑上,每秒大约能够移动6~8百万步。
虽然64个盘子超出了人力和现代计算机的能力,但至少对于计算机来说,这不是一个无法完成的任务,因为与我们人类不同,计算机的能力在不断提高。
分解问题
一股脑地考虑每一步如何移动很困难,我们可以换个思路。先假设除最下面的盘子之外,我们已经成功地将上面的63个盘子移到了b柱,此时只要将最下面的盘子由a移动到c即可。如图:
当最大的盘子由a移到c后,b上是余下的63个盘子,a为空。因此现在的目标就变成了将这63个盘子由b移到c。这个问题和原来的问题完全一样,只是由a柱换为了b柱,规模由64变为了63。因此可以采用相同的方法,先将上面的62个盘子由b移到a,再将最下面的盘子移到c……对照下面的过程,试着是否能找到规律:
也许你已经发现规律了,即每次都是先将其他圆盘移动到辅助柱子上,并将最底下的圆盘移到c柱子上,然后再把原先的柱子作为辅助柱子,并重复此过程。
这个过程称为递归,即定义一组基本操作,这组操作将规模小一点(或大一点)的操作当做一个整体——无需关心它的细节,只当它已经完成了——然后执行剩下的操作。而在更小或更大的规模中也依此操作,直到规模达到预定值。
在数学上,有些公式就是采用递归的方式定义的。例如阶乘和斐波那契数列(Fibonacci Sequence)。前者的公式为:
规定0!=1!=1,对于n>=2,有n!=n*(n-1)!
这里的n-1就是比n规模略小的阶乘,而1就是规模的最小值(预定值)(0是作为特殊值而专门规定的)。
著名的斐波那契数列定义如下,可以看出,f(n)是由规模更小一些的f(n-1)和f(n-2)推导出来的:
f(0)=0,f(1)=1
f(n)=f(n-1)+f(n-2) (n>=2)
因此,递归实际上就是用自己来定义自己。
回到前面汉诺塔的问题上来。我们假设函数func(n, a, b, c)用于将n个圆盘由a移动到c,b作为辅助柱子。那么我们可以这样实现这个递归过程:
func:
if n!=0 then ;预定值 func(n-1, a, c, b) ;将n-1个盘子由a移动到b,以c为辅助柱子(注意参数顺序) move a[n] to c ;将a上的最后一个盘子移动到c func(n-1, b, a, c) ;将n-1个盘子由b移动到c,以a为辅助柱子 endif ;完成
func中有两个递归调用,它们的规模刚好比n小1。注释说明了每行代码的作用和意图。正如注释里所强调的那样,注意参数的顺序——参数位置不同,其代表的意义也不一样。
第一个递归调用以c作为辅助柱子,这没有问题,因为c柱子的最下面的k个圆盘一定是所有圆盘中最大的k个,因此将其作为辅助柱子不会出现大圆盘在小圆盘之上的情况。
程序实现
下面是使用Java实现的汉诺塔程序,程序使用Stack实例来保存每个柱子上的盘子及它们的顺序。Stack是队列的一种,其中的元素遵循“先进先出”(FIFO)的原则,即不允许从队尾取元素。这种队列通常也称为“栈”。栈对元素的进出约定与汉诺塔的规则一致。
resolve方法用来移动盘子,参数n表示要移动的盘子的数量,a是盘子所在的柱子,b是辅助柱子,c是目标柱子。注意此方法会首先检查参数n,当n为0时直接返回,这就是前面所说的“预定值”。如果没有对预定值的判断,resolve的递归过程将不会自然终止,而是无限进行下去,直到塞满系统内存堆栈而导致程序奔溃。
另外要注意的是程序将盘子的初始数量设为32个,你可以修改该值,但建议不要设置的过大,原因正如前面所计算的那样,如果采用64个圆盘,你将至少需要数百年才能看到结果(更可能的结果是由于步数太多,系统没有足够的内存而导致程序奔溃)。
import java.util.Iterator; import java.util.Stack; public class HanoiTower { public static void print(Stack<Integer> s) { Iterator<Integer> i = s.iterator(); while (i.hasNext()) { System.out.printf("%d ", i.next()); } System.out.println(); } public static void resolve(int n, Stack<Integer> a, Stack<Integer> b, Stack<Integer> c) { if (n==0) return; resolve(n-1, a, c, b); c.push(a.pop()); resolve(n-1, b, a, c); } public static void main(String[] args) { int count = 32; Stack<Integer> a = new Stack<Integer>(); Stack<Integer> b = new Stack<Integer>(); Stack<Integer> c = new Stack<Integer>(); for (int i=count; i>0; i--) { a.push(i); } print(a); long start = System.currentTimeMillis(); resolve(count, a, b, c); long end = System.currentTimeMillis(); print(c); System.out.println((end - start)/1000); } }
在我的笔记本电脑上运行该程序,消耗的时间统计如下:(Intel Core i3 3.2GHz处理器,2.2GHz 3GB内存)
正如备注中所显示的,步数是圆盘数量的指数函数,即steps=2^n - 1,运行所需时间也遵从这个规律。
扩展:汉诺塔问题的非递归实现
理论上来说,递归算法都能够改为循环来完成。例如阶乘问题,既可以用递归定义给出,也可以采用下面的方式来定义:
规定0的阶乘为1。对于其他自然数,n的阶乘可以表示为:
n!=1*2*3*...*n
这种方式实际上就是采用了循环来定义。
然而,并不是所有的递归都能简单直观地改写为循环,例如前面所介绍的斐波那契数列的定义,和本文所讨论的汉诺塔问题。
下面这个帖子介绍了不使用递归而是用循环来解汉诺塔问题的算法。
http://tieba.baidu.com/f?kz=1255166419
程序框架
研究如何使用并行算法解决汉诺塔问题。例如,64个盘子,每次成功将一个盘子移动到目标柱子上的过程都是独立的。因此可以分别并行地计算。
不过需要注意的是,成功移动第一个盘子的步数是最多的,占到总步数的1/2;而第二个盘子需要总步数的1/4……最后一个盘子仅需1步。
所以在实现并行方式时要考虑这种差异。而不是简单地使每个并行分支移动相同数量的盘子。
Dijkstra算法_北京地铁换乘_android实现 android 2.2+
直接上图片 如下:
Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。
Dijkstra算法是很有代表性的最短路算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。
其基本思想是,设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。
初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。
/**时间复杂度比较复杂,因为换乘结点的关系导致的 最坏情况下(每个站之间都有连线,但是地铁线路图实际上是不存在次情况的):O(2^n) 相反 最优情况下(之间只有唯一的连接点,次情况下也不是很现实的,有的地铁换乘是多个换乘点都在同一条线上的) 此时用hashtable所以是:O(1) */ private java.util.HashSet<String> GetF(java.util.HashSet<String> beginlist) { if (mainht == null || mainht.isEmpty()) { return null; } returnlist = new java.util.HashSet<String>(); if (beginlist.isEmpty()) { isend = 1; } else { /**O(n) */ for (String strbegin : beginlist) { if (strbegin.indexOf("-") == -1 && mainht.containsKey(strbegin) == true) //have this key and first load data { bxy = (double[])DCht.get(strbegin); earry = mainht.get(strbegin).toString().split("[,]", -1); for (String ar : earry) { exy = (double[])DCht.get(ar); isadd = CK(isadd, bxy, exy); if (isadd == true) { returnlist.add(strbegin + "-" + ar); isend = 0; } } } else if (strbegin.indexOf("-") > -1 && mainht.containsKey(strbegin.substring(strbegin.lastIndexOf("-") + 1)) == true) { temgstr = strbegin.substring(strbegin.lastIndexOf("-") + 1); bxy = (double[])DCht.get(temgstr); earry = mainht.get(temgstr).toString().split("[,]", -1); //exchange node for (String ar : earry) { exy = (double[])DCht.get(ar); isadd = CK(isadd, bxy, exy); if (isadd == true) { if (!strbegin.contains(ar)) { returnlist.add(strbegin + "-" + ar); } isend = 0; } } } } } earry = null; if (isend == 0) { return GetF(returnlist); } else { return null; } }
//East South West North Northeast Northwest Southeast Southwest
private
boolean
CK(
boolean
isadd,
double
[] bxy,
double
[] exy)
{
return
true
;
}
|
整个查询过程耗时不超过20毫秒
单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子结构性质。
一.最短路径的最优子结构性质
该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。
假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。
二.Dijkstra算法
由上述性质可知,如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点。那么(Vi...Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist[i]+matrix[i][j]}。根据这种思路,
假设存在G=<V,E>,源顶点为V0,U={V0},dist[i]记录V0到i的最短距离,path[i]记录从V0到i路径上的i前面的一个顶点。
1.从V-U中选择使dist[i]值最小的顶点i,将i加入到U中;
2.更新与i直接相邻顶点的dist值。(dist[j]=min{dist[j],dist[i]+matrix[i][j]})
3.知道U=V,停止。