现在的位置: 首页 > 综合 > 正文

88

2013年09月13日 ⁄ 综合 ⁄ 共 9549字 ⁄ 字号 评论关闭

原文地址:

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

  1. #!/usr/bin/env python   
  2.   
  3. def conflict(state, nextX) :  
  4.     nextY = len(state)  
  5.     for i in range(nextY) :  
  6.         if abs(nextX - state[i]) in (0, nextY - i) :  
  7.             return True  
  8.     return False  
  9.   
  10. def queenes(num=8, state=()) :  
  11.     for pos in range(num) :  
  12.         if not conflict(state, pos) :  
  13.             if len(state) == num - 1 :  
  14.                 yield (pos, )  
  15.             else :  
  16.                 for result in queenes(num, state + (pos, )) :  
  17.                     yield (pos, ) + result  
  18.   
  19. def printState(state) :  
  20.     def printLine(pos) :  
  21.         print '. ' * pos + 'X ' + '. ' * (len(state) - pos - 1)  
  22.     for pos in state :  
  23.         printLine(pos)  
  24.     print  
  25.   
  26. total = 0  
  27. for state in queenes() :  
  28.     printState(state)  
  29.     total += 1  
  30. print total  

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

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

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

  1. #include <vector>   
  2. #include <cstdlib>   
  3. #include <iostream>   
  4.   
  5. using namespace std;  
  6.   
  7. static int total = 0;  
  8.   
  9. void printState(const vector<int> &state)  
  10. {  
  11.     for(int i = 0; i < state.size(); ++i)  
  12.     {  
  13.         int count = state[i];  
  14.         while(count-- > 0)  
  15.             cout << ". ";  
  16.         cout << "X ";  
  17.         count = state.size() - state[i] - 1;  
  18.         while(count-- > 0)  
  19.             cout << ". ";  
  20.         cout << endl;  
  21.     }  
  22.     cout << endl;  
  23.     ++total;  
  24. }  
  25.   
  26. bool conflict(const vector<int> &state, int nextX)  
  27. {  
  28.     int nextY = state.size();  
  29.     for(int i = 0; i < nextY; ++i)  
  30.     {  
  31.         int t = abs(nextX - state[i]);  
  32.         if(t == 0 || t == nextY - i)  
  33.             return true;  
  34.     }  
  35.     return false;  
  36. }  
  37.   
  38. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  39. {  
  40.     int pos = 0;  
  41.     for(;;)  
  42.     {  
  43.         while(conflict(state, pos))  
  44.         {  
  45. popitem:  
  46.             while(pos == num - 1)  
  47.             {  
  48.                 if(state.empty())  
  49.                     return;  
  50.                 pos = state.back();  
  51.                 state.pop_back();  
  52.             }  
  53.             ++pos;  
  54.         }  
  55.         state.push_back(pos);  
  56.         if(state.size() == num)  
  57.         {  
  58.             printState(state);  
  59.             state.pop_back();  
  60.             goto popitem;  
  61.         }  
  62.         pos = 0;  
  63.     }  
  64. }  
  65.   
  66. int main()  
  67. {  
  68.     nqueenes();  
  69.     cout << total << endl;  
  70.     return 0;  
  71. }  

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

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

  1. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  2. {  
  3.     int pos = 0;  
  4.     for(;;)  
  5.     {  
  6.         while(conflict(state, pos))  
  7.         {  
  8.             ++pos;  
  9.         }  
  10.         state.push_back(pos);  
  11.         pos = 0;  
  12.     }  
  13. }  

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

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

  1. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  2. {  
  3.     int pos = 0;  
  4.     for(;;)  
  5.     {  
  6.         while(conflict(state, pos))  
  7.         {  
  8.             ++pos;  
  9.         }  
  10.         state.push_back(pos);  
  11.         if(state.size() == num)  
  12.         {  
  13.             printState(state);  
  14.         }  
  15.         pos = 0;  
  16.     }  
  17. }  

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

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

  1. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  2. {  
  3.     int pos = 0;  
  4.     for(;;)  
  5.     {  
  6.         while(conflict(state, pos))  
  7.         {  
  8.             while(pos == num - 1)  
  9.             {  
  10.                 pos = state.back();  
  11.                 state.pop_back();  
  12.             }  
  13.             ++pos;  
  14.         }  
  15.         state.push_back(pos);  
  16.         if(state.size() == num)  
  17.         {  
  18.             printState(state);  
  19.         }  
  20.         pos = 0;  
  21.     }  
  22. }  

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

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

  1. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  2. {  
  3.     int pos = 0;  
  4.     for(;;)  
  5.     {  
  6.         while(conflict(state, pos))  
  7.         {  
  8. popitem:  
  9.             while(pos == num - 1)  
  10.             {  
  11.                 pos = state.back();  
  12.                 state.pop_back();  
  13.             }  
  14.             ++pos;  
  15.         }  
  16.         state.push_back(pos);  
  17.         if(state.size() == num)  
  18.         {  
  19.             printState(state);  
  20.             state.pop_back();  
  21.             goto popitem;  
  22.         }  
  23.         pos = 0;  
  24.     }  
  25. }  

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

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

  1. void nqueenes(int num = 8, vector<int> &state = vector<int>())  
  2. {  
  3.     int pos = 0;  
  4.     for(;;)  
  5.     {  
  6.         while(conflict(state, pos))  
  7.         {  
  8. popitem:  
  9.             while(pos == num - 1)  
  10.             {  
  11.                 if(state.empty())  
  12.                     return;  
  13.                 pos = state.back();  
  14.                 state.pop_back();  
  15.             }  
  16.             ++pos;  
  17.         }  
  18.         state.push_back(pos);  
  19.         if(state.size() == num)  
  20.         {  
  21.             printState(state);  
  22.             state.pop_back();  
  23.             goto popitem;  
  24.         }  
  25.         pos = 0;  
  26.     }  
  27. }  

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

  曾写过一个类似于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》。

抱歉!评论已关闭.