回溯法是剪了枝的穷举,这是字面上的说法,不太好理解,不如讲解实例来的酸爽,于是引出了N阶可达问题:
有N个国家,每个国家有若干城市,小明要从中国(任意一个城市)出发,遍历所有国家(假设这个遍历顺序已经定了),最终到达美利坚(任意一个城市)。而城市之间有可能不可达,只有小明尝试过才知道(就是后面的check()函数),求满足要求的一条路径?
从上面的表述中我们已经嗅到了浓浓的穷举屌丝气质——遍历所有组合,但是我们的回溯思想总是基于这样一个简单的事实:如果当前选择导致你走进了死胡同,那么这个选择一定是错误的,同时基于这个错误的后续所有的选择都是错误而无意义的(剪枝)。道理的前半句表明我们要及时回溯,而后半句指出了这样做的优点是剪枝。比如小明要遍历中国---日本---美国,小明选择从中国武汉出发,这个选择是正确的还是错误的尚不明确,但是小明经过许多个check()之后发现,没有从武汉到日本任意一个城市的可达线路,这说明选择从武汉出发这个决定是错误的,应该回溯,重新选择一个中国的起点城市,小明在不知不觉中已经排除了形如(中国武汉市)---(日本XX市)---(美国XX市)的诸多组合,这就是所谓的剪了枝的穷举。
数独问题也是典型的N阶可达问题,下面以一个挖去64个洞的数独为例来具体说明,每个洞有1~9共9种可能性,一共要填64个洞,并且每填写好一个洞对后续的步骤会产生影响。
假设我们是从左到右,从上到下依次填写数字,那么此数独问题可以表达为如下的64阶可达问题:
根据回溯思想,采用递归函数(因为每层的情况是一样的,请读者思考如果不一样该如何编程)依次处理编号为0~80共计81个格子(代码略)。
下面我们用号称世界最难的数独题来测试一下程序,首先在程序同目录下建立sudoku.txt的文件,然后输入以下内容:
800000000
003600000
070090200
050007000
000045700
000100030
001000068
008500010
090000400
保存然后运行程序,得到如下结果。程序大约运行了60ms,在进行了49584次尝试之后找到了数独的解。
再来一发,号称专杀暴力破解的数独题试一下:
哇,好奇怪,世界最难数独都能在百毫秒内求解,为什么这个数独题居然花了大约37秒?莫非此数独真的有专杀暴力破解的神秘力量?前面我们说回溯只是做了剪枝的工作,下面这幅图展示了回溯到底做了什么:
剪枝后只用搜索红色部分
从本质上说,搜索是一维的,答案位于这个一维地图中的某处,你搜索速度的快慢取决于你的地图和答案在地图中的位置。前面我们尝试的顺序是123456789,现在我们尝试987654321这个搜索顺序,把程序修改一番,再次运行哪个两个数独,结果如下:
左为世界最难,右边为专杀暴力
上面我们只是改变了搜索顺序,只是一维地图的反转后搜索,相当于地图没变我们换了另一头搜索。在第一程序中,之所以很快能搜索到世界最难数独,而搜索专杀爆破很慢,是因为世界最难的答案离这一端很近,专杀爆破的答案离这一端很远。当我们在不改变地图的情况下换另一端开始搜索,就会出现上面的结果。
故在数独算法中,每个人都说自己的算法是高效的,并给出试验数据。实际上这个说法并不准确,只能说采用不同的方法,地图的规模不同,答案所在的位置也不同。对于同一个数独,你的算法你别人快并不能说明什么,因为必然存在专杀你这种搜索方法的数独存在。你的算法搜索的越快,换个姿势一定就会越慢。