8皇后问题Python与C++实现

  今天看《Python基础教程(第二版)》,看到生成器部分,作者用生成器给出一种非常精妙的解法:

  非常受启发,想去网上找更多资料,正好找到了这位网友的博客“http://blog.csdn.net/lwj1396/archive/2008/12/31/3663656.aspx”。原来他也正好是看了“8皇后问题”的Python生成器实现有感而发写了一个C++实现,并对两者进行了对比。

  仔细阅读了该网友的C++代码和对比,非常受教。下面是我的几点感受。

  首先,该网友的C++实现只求一个解,并不输出全部的解,所以我想在其基础上写一个C++的实现以输出全部的解,经过折腾总算弄出来了,

  我的代码还算简洁,其中有一点不太舒服的是不仅用了一个goto,而且这个goto直接跳入了循环的内部,可以说与通常结构化程序设计原则相差得太大了。不过没办法,谁让C++没有Python的yield支持呢,我们只能用堆栈和goto去实现之。当然这个goto其实并非必须,但若想将代码写得像Python那样简洁,还是必要的。在日常写程序中goto用得还真不多,但做算法时有时goto用起来反而更方便,忘了是哪本书说的了,说是用goto去直接翻译算法(如TAOCP上的那些成步骤的,又常有转步骤几的那种)是最简单、最直接、最准确的方法。在前几天读TinyScheme的源代码,它使用了TAOCP上的一个垃圾回收算法,就是用的goto直接翻译的,效果非常不错,所以今天我也果断在代码中用了goto。

  其次,我对nqueenes这个函数的C++实现还深有触。Python中有yield语句/表达式支持,代码可以写得很直接且高效,而C++中就没有这好运气了,只能用堆栈自己去做,不过发现其实还有些规律可循。我的这个nqueenes函数其实是一步做起来的,堆栈维护的当前迭代状态不要去轻易改变之,在框架代码改变了这个状态后,立即进行相关判断并采取一定动作,如我先写

这个可以说是最基础的框架代码:如果当前欲插入的位置冲突,我们就增加pos并尝试下一个位置,直到遇到一个不冲突的位置(注意我们忽略了pos过大以致超出了num - 1,这是我们接下来要考虑的),这时将“皇后”填到这个位置,并将尝试的位置恢复成0。下一步我们要对“有副作用”的表达式——即改变状态的表达式作进一步处理。

  首先是state.push_back(pos);这里改变了堆栈的状态,我们需要检测,因为当state.size()==num的时候,我们其实已经获得了一个解,这时我们应“收集”这个解:

  注意,printState之后直接到pos=0直接执行是不对的,这个后面再说。

  下一个有“副作用”的地方是++pos;很明显这个表达式有可能将pos送到0...num-1这个合法的范围之外,我们需要应对这种情况,这样就有了前示的另一个嵌入的循环。当pos==num-1时,pos不能再增加,相反这表明堆栈state维护的当前状态不能获得一个解,我们需要“回溯”,这里即是从栈中弹出一个值:

  注意,弹出一个值要了两条语句,本来一条是很自然的,但STL从效率考虑却不这样做,个人对此耿耿于怀。每弹出一个值就回到了一个之前的状态,pos与state都得到了更新,但pos仍有可能是num - 1,此时就需要继续弹出值。为什么是先判断pos == num - 1,然后++;而不是先++,再判断pos == num - 1?嗯,这个也是经过折腾想到的最简洁的写法。

  经过上面的回溯的加入,基本的框架已经形成,现在我们来完成printState的善后事宜。仔细想容易知道——在获取一个解将其printState后,为了获取下一个解,只须“假装”当前被push的pos是无效的,将程序的流程转向conflict失败之处即可。下面即是它的思路,用了一个“变态”的goto:

  注意,在goto转移流程之前,我们pop掉了刚才push进去的pos,因为我们将这个pos当成“无效”的,因此这样做是显然的。

经过上面的处理,程序还有最后一个地方需要处理,那就是在内层回溯弹出元素时,有可能将堆栈弹空,这是一个极端情况,意味着我们已经找完了所有的解,不能再找到解,此时应结束算法,下面是完成了的nqueenes函数:

  上面对这个简单的函数说了一大通,可能你已经烦了,其实我自己都烦了。一言以蔽之——留意状态改变的地方。这一条是写这个程序最大的感受,只要我们留意程序状态的改变,一个最基本的算法框架就可以将我们带到算法的完整实现

  曾写过一个类似于printf的函数format,这个函数像printf一样格式化字符串,并将结果用动态分配的字符串返回(就像gcc中的asprintf一样)。format的实现实际是一个手写的递归下降分析器,它们的本质也是要维护好程序的“状态”,一定不要轻易使用“赋值”语句(++也是赋值),在“语法分析”过程中我们可以用各种判断,但是改变状态(使用++改变状态,将语法分析的当前token——格式字符串的当前字符移至下一个)一定发生在已经确定了语法结构的时候!

  还想到了更远的地方,在SICP这本书中甚至到了第三章(总共五章)才正式引入赋值,而且真正的赋值其实就是一个set!(即是说任何一种程序设计语言,一旦引入赋值——只需要引入最简单的一种赋值运算,语言就将发生根本的、彻底的改变),所以不要小看了赋值,赋值与否是一个根本的问题。函数语言独树一帜,就是因为它们限制,甚至完全消除了赋值。

  最后一点我想说,Python的“生成器”所实现的程序设计方法,实则与Lisp(包括Scheme)中用惰性计算实现的“流”异曲同工。第一次接触到“流”这个概念正是在SICP这本书中,使用“生成器”或“流”我们可以造出概念上的无穷序列,这样某些程序的思路将变得无比简单。Scheme这个Lisp方言虽小,但它五脏俱全,Python费好大劲才建立起来了“生成器”这个好用的工具,而Scheme一个简单的宏就可以搞定这一切,这不得不令人对Scheme/Lisp的强大叹为观止,所以Lisp语言的确非常值得学习,强力推荐《Structure and Interpretation of Computer Programs》与《Practical Common Lisp》。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值