Scheme来实现八皇后问题(2)
版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/9790466.html 作者:窗户 QQ/微信:6679072 E-mail:6679072@qq.com
上一章讲了用1~n的排序来表示n皇后的解,然后通过枚举1~n所有的排列、判定谓词过滤所有排列得到最终的所有解。
在此基础上,这一章我们思考是否存在更好的解法,从而深化这个问题。
效率问题
为了测试效率,在代码末尾加上
(queen (read))
表示解决的皇后个数由输入的结果决定。
还是先把Scheme代码编译、链接为普通可执行文件,这样运行就不是在解释的条件下了,速度可以提升数倍。
八皇后使用这个程序出来结果还算可以接受,但是当我想解决10皇后问题的时候,却花了半分钟,而如果是解决11皇后问题,我等待了好几分钟,系统把进程杀了。
我们可以意识到,程序的效率似乎并不是那么高。那么有没有提升的办法呢?
想要找到提升的办法,我们先要分析之前的算法慢的原因。
这个算法中,内存使用姑且就不说了(其实存储所有的排列需要很大的内存),我们要产生所有的排列,然后每一个排列都要单独判定是否符合条件。
然而,我们想一想,我们真的需要每个排列都独立检查一遍吗?
实际上,我们可能真的不需要如此。我们其实只需要检测到一个片段上存在皇后互吃,那么包含这个片段的所有排列自然都不可能是解,可以直接被排除。
比如我们检测到如果一个排列以1、2开头的话,那么这两个点距离为1,值也相差1,两个皇后互吃,从而就可以知道,所有以1、2开头的排列都不需要检测了。
也就是所有的(1 2 ...)排列直接因为我检测到(1 2)不能作为解而直接排除。
这样从片段来对所有排列剪枝,当然比挨个检测效率高。
字典顺序
我们要考虑一个字典顺序的检测。
字典顺序就是按照英文字典那样,单词出现的顺序是按字符串的大小顺序。

字符串的大小比较,大家应该都很熟。
两个字符串从头逐位比较,过程中,对应位的字符相等则继续比较,直到过程中一个字符串先到尾部或者字符上分出大小,先到尾部或者对应位上的字符小的一方的字符串较小,另一个字符串则较大。如果两个字符串同时都检测到了尾部,那么两个字符串当然一模一样,则为相等。
C语言中字符串比较可以用strcmp函数,而Scheme里字符串比较可以用 string=? string>? 等函数。
但是,我们这里是要对于所有排列按照字典顺序检测,可是这里每个排列是一个数字组成的list,那么我们可以按照数字的大小来替代之前字符串比较时的字符大小,就可以做到字典序列了。
比如1~3的全排列一共有6个排列,按照字典顺序从小到大本应该如下:
(1 2 3)
(1 3 2)
(2 1 3)
(2 3 1)
(3 1 2)
(3 2 1)
但考虑到列表的特殊结构,我们判断两个排列的大小从最后一位开始看的话(也就是列表反过来看),在这里因为一路可以使用cons/car/cdr而不是append/take/drop之类相对复杂的递归,从而要方便很多,效率也要高,于是上述1~3的全排列按照字典顺序会如下:
(3 2 1)
(2 3 1)
(3 1 2)
(1 3 2)
(2 1 3)
(1 2 3)
实际的例子
我们需要实际来看看按照字典序列并加上之前的剪枝思路如何完成完整的解答。
在这里,我们可以用4皇后来作为i例子。
我们试图要用迭代完成我们的检测,用当前检测的列表和已有的解合成迭代的状态。
最开始的时候,我们要检测的list是空列,而解初始也为一个空列
目前 解 说明
() () 初始
(1) () 检测合法
(2 1) () 不合法,所以以(2 1)结尾的排列都不合法
(1) () 因为上面不合法,所以2被退掉
(3 1) () 填入比刚才退掉的2大的数中最小的
...
这里产生了一个问题,上面红字标注的两行,除了说明之外,其他一模一样,无法区分。而一个转化成(2 1)()状态,另外一个转化为(3 1)()状态,相同的状态转换成了不同的状态,对于迭代,
这个完全没道理!
说明状态还不完善,我们刚才两次从(1)()状态出发,之所以第一次转化到了(2 1)()状态,而第二次转化到了(3 1)()状态,原因就在于其实都是在(1)的基础上,找了原则上最小的值。从而,我们可以在状态中再加一个限值,标志着列表下一个添加的元素的选取必须高于这个值。
这样状态才是完备的。
于是上面4皇后问题状态的转换可以如下:
目前 限值 解 说明
() 0 () 初始
(1) 0 () (1)合法,升位
(1) 2 () (2 1)非法 (满足的最小的数是2)
(3 1) 0 () (3 1)合法,升位
(3 1) 2 () (2 3 1)非法
(3 1) 4 () (4 3 1)非法
(1) 3 () 不存在值,降位
(4 1) 0 () (4 1)合法,升位
(2 4 1) 0 () (2 4 1)合法,升位
(2 4 1) 3 () (3 2 4 1)非法
(4 1) 2 () 不存在值,降位
(4 1) 3 () (3 4 1)非法
(1) 4 () 不存在值,降位
() 1 () 不存在值,降位
(2) 0 () (2)合法,升位
...
(3 1 4 2) 0 () (3 1 4 2)合法,升位
(1 4 2) 3 ((3 1 4 2)) 加入解,降位
...
(4) 3 ((2 4 1 3) (3 1 4 2)) 不存在值,降位
() 4 ((2 4 1 3) (3 1 4 2)) 不存在值且无法降位,结束
状态转移
现在,我们定下状态为 目前,限值,解
我们来分析关于状态的一切:
初始时,状态为 ()0()。
结束时,一定是因为目前的是空列,而且限值已经达到了n皇后的n。
当目前的列表包含了1~n的数时(其实就是长度为n),那么找到了一个解,把这个列表加入到解,然后降位,也就是目前的列表把最前面的一位去掉,然后限值设为最前面的这一位。
其他情况下,找剩余的数中大于限制的最小数:
(1)如果不存在,则降位。
(2)如果存在,假如这个值加到目前的列表前得到的新表是合法的,那么升位,新列表作为目前列表,限值设为0即可。
(3)如果存在,假如这个值加到目前的列表前得到的新表是非法的,那么限制调整为刚才找到的最小数。
迭代和封装
按上述规则,迭代代码如下:
(define (_queen n current gt result)
(cond
((and (null? current) (>= gt n)) result);终止条件
((= n (length current)) (_queen n (cdr current) (car current) (cons current result)));记下解
(else
(let (
(remained (filter (lambda (x) (> x gt )) (remove* current (range (+ n )))) );剩下的数中哪些是大于下限的
)
(if (null? remained)
(_queen n (cdr current) (car current) result)
(let ((next (car remained)))
(if (valid? current next)
(_queen n current next result)
(_queen n (cons next current) result))))))))
其中,remove*是racket里的函数,用于集合相减,并且不改顺序,但它并不属于Scheme标准。此处略去实现。
比如(remove* '(2 3 4) '(1 2 4 5 6))返回'(1 5 6)。
当然,不忘封装一下:
(define (queen n) (_queen n '() 0 '()))
合法性检测
检测所用的valid?函数并未实现。
在这个算法中,如果一个序列是非法的,也就是存在皇后互吃的,一定是最新的元带来的。因为如果判断到这一步,那么之前的子序列一定是合法的。
这是一个递归的思路,也是比上一章使用迭代来检测一个排列所有的可能更加快速的地方之一。
比如要检测'(2 5 3 1)是不是合法,在检测前,我们无条件知道(cdr '(2 5 3 1))也就是'(5 3 1)是合法的。
那么合法性判断就成了看最左边的2和后面的3个元素是否有差值的绝对值等于距离,一种比较好的做法是:
先将'(5 3 1)的每个元素减去新加的2,得到'(3 1 -1),再取绝对值得到'(3 1 1),再和'(1 2 3)比较,看看是否在相同位置有相同元素。
判断两个列表存在不存在相同位置有相同元素,用个递归很容易写:
(define (same_ele_pos? x y)
(cond
((null? x) #f)
((= (car x) (car y)) #t)
(else (same_ele_pos? (cdr x) (cdr y)))
)
)
于是合法性检测就可以写成如下:
(define (valid? lst new)
(same_ele_pos?
(range (+ (length lst)))
(map (lambda (x) (abs (- x new))) lst)
)
)
上面检查'(2 5 4 1)是否合法,就可以通过(valid? '(5 4 1) 2)来检测。
测试
把上述代码后面加(queen 10)解决10皇后问题,编译之后,我们发现运行时间连1秒都不需要。
而如果要求12皇后问题需要20秒。实际上,我们还可以在状态中引入一些别的东西以提高速度,从而使得运行时间变成现在的几分之一,但这已经不是我想在这里讲的了。
算法这东西,很多时候很难做到极致,所以工程中有一个方向就是随着时间推移,软件版本在提升算法运行速度,比如PCB/FPGA的布线。工程师们依然为之不断努力。
Scheme来实现八皇后问题(2)的更多相关文章
- Scheme来实现八皇后问题(1)
版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖.如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/9768105.html 作者:窗户 Q ...
- 《sicp》八皇后谜题
<sicp>八皇后谜题 书中练习2.42.八皇后谜题问的是如何将八个皇后摆在国际象棋棋盘上,使得任意一个皇后都不能攻击另一个皇后(也就是说任意两个皇后都不能在同一行,同一列和同一对角线上) ...
- 八皇后算法的另一种实现(c#版本)
八皇后: 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于 ...
- 数据结构0103汉诺塔&八皇后
主要是从汉诺塔及八皇后问题体会递归算法. 汉诺塔: #include <stdio.h> void move(int n, char x,char y, char z){ if(1==n) ...
- Python学习二(生成器和八皇后算法)
看书看到迭代器和生成器了,一般的使用是没什么问题的,不过很多时候并不能用的很习惯 书中例举了经典的八皇后问题,作为一个程序员怎么能够放过做题的机会呢,于是乎先自己来一遍,于是有了下面这个ugly的代码 ...
- Python解决八皇后问题
最近看Python看得都不用tab键了,哈哈.今天看了一个经典问题--八皇后问题,说实话,以前学C.C++的时候有这个问题,但是当时不爱学,没搞会,后来算法课上又碰到,只是学会了思想,应该是学回溯法的 ...
- OpenJudge1700:八皇后问题 //不属于基本法的基本玩意
1700:八皇后问题//搜索 总时间限制: 10000ms 内存限制: 65536kB 描述 在国际象棋棋盘上放置八个皇后,要求每两个皇后之间不能直接吃掉对方. 输入 无输入. 输出 按给定顺序和 ...
- C#八皇后问题 枚举值
记得刚出道的时候, 有考虑怎么面试, 以及可能会遇到的面试题, 有一个人说了一下 八皇后问题, 据说要用 sql 语句写出来, 暂时我 写了一个C#版本的, 经测验,八皇后算法结果为 92种, 这个与 ...
- 八皇后(dfs+回溯)
重看了一下刘汝佳的白板书,上次写八皇后时并不是很懂,再写一次: 方法1:逐行放置皇后,然后递归: 代码: #include <bits/stdc++.h> #define MAXN 8 # ...
随机推荐
- 【转】Kali更新源
1.切换到root用户(如果已经是root用户就直接看第二步) dnt@Kali:~$ su 2.用vim打开sources.list,手动添加下面的更新源 root@Kali:~# vim /etc ...
- java基础(十七)----- 浅谈Java中的深拷贝和浅拷贝 —— 面试必问
假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short, ...
- C#版(击败100.00%的提交) - Leetcode 744. 寻找比目标字母大的最小字母 - 题解
C#版 - Leetcode 744. 寻找比目标字母大的最小字母 - 题解 744.Find Smallest Letter Greater Than Target 在线提交: https://le ...
- Unix程序员的Win10二三事
macOS延续自BSD Unix, Linux则是从内核开始重新编写但延续Unix使用方式的Unix.所以mac还有linux程序员,一般都算是*nix程序员,尽管其中还有不少的区别. Windows ...
- Paramiko模块简单使用
介绍 Paramiko 一个第三方包,需要单独安装我们知道远程批量主机管理,比如ansible.Fabric,不需要安装客户端的远程执行命令等,这些都是基于Python原生的SSH,相当于模拟了一个S ...
- 使用mpvue开发小程序教程(三)
在上一篇文章中,我们熟悉了一下通过vue-cli生成的mpvue工程代码骨架的基本结构,大致了解了每一个部分的代码到底要放到何处.从本文起我们就开始涉及真正的编码部分,学习使用Vue的语法去编写小程序 ...
- Linux服务器时间相关命令记录
前言 以往安装服务器时间都是正常,但是最近服务器的时间经常出现问题,所以在安装配置完成服务器之后需要对服务器的时间进行测试,如果服务器时间异常,那么当程序去取系统时间的时候就会出现问题. 时间相关命令 ...
- 痞子衡嵌入式:飞思卡尔Kinetis系列MCU启动那些事(2)- KBOOT形态(ROM/Bootloader/Flashloader)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔Kinetis系列MCU的KBOOT形态. 痞子衡在前一篇文章里简介了 KBOOT架构,我们知道KBOOT是一个完善的Bootl ...
- VisualStudio移动开发(C#、VB.NET)Smobiler开发平台——GifView控件的使用方式
一. 样式一 我们要实现上图中的效果,需要如下的操作: 从工具栏上的“Smobiler Components”拖动一个GifView控件到窗体界面上 修改GifView的属性 Aut ...
- oracle账户登录数据库进行如下操作:
CREATE USER NORTHBOUND IDENTIFIED BY NORTHBOUND DEFAULT TABLESPACE "TBS_DNINMSV31" TEMPORA ...