• n皇后详解及代码实现/C++


    初衷

    这个学期开了算法课,要几个关键算法思想的代码实现。当时感觉学的还可以了,也做了认真的笔记。真正写代码的时候发现还是

    没有完全掌握。网上关于这方面的资料也零零散散不是很全,致使走了不少弯路。今晚上实验成功验收了,感觉自己也收获不小

    遂决定把算法实现的详细思路记录下来,一是自己坐下总结、另外也希望给当时想我一样找资料、搞算法的同学一些帮助。

    这中间我会尽最大可能的把问题描述清楚。

    这篇博文主要写的是n皇后问题、后续还会加上背包问题(动态规划和分支界限)、旅行商问题等等。

     

    写在前面

    不管什么问题、都是可以抽象的,对于任何问题,你总是可以找到几个point,它们对问题全局有着决定性的作用,弄清楚看他们之间的内在联系;

    还有一个重要的方式就是找特例:对于一个无从下手的问题,可以举几个例子,找特例。通过几个关键的point和特例,你就能容易的找出隐藏在问

    题背后的实质。

    在这里我会给出这个问题的分析过程、而不会用成熟的理论来说明问题。如果你只想要代码的话也可以直接copy。

    问题描述:

    一个n*n的棋盘,要在上面放n个皇后。规则:两个皇后之间如果是同列、同行、同对角线它们会互相攻击。也就是说:棋盘上的任意两个皇后皇后

    不能为同列、同行、同对角线。

    问题分析

    对于这个问题、当n不大的时候,可以用穷举法实现。对于n皇后,每一行有n个位置可以放,一共n行。就会有n的n次方种情况。对于这些情况、再一一判断是不是满足情况。

    其实一个关键的点在于:什么时候判断已经放了皇后的棋盘是否满足条件,大致可以分为两种:

    1、等棋盘上放了n个皇后以后判断

    2、放一个皇后判断一次、对于特定的某一次,如果这种情况不满足条件,那么以这种情况为基础而生成的情况就不用再判断了,他们都不会满足条件。

    比如说:头两行有冲突,那么后面的不管怎么放,都没有意义了,总会有冲突。

    如下图:

    说明:这是四皇后的图解,树的每一层对应棋盘的一层,树的边上的数字代表放在棋盘的位置,如:边上的数字是3,则代表下一行的皇后位置

    是第三行,一次类推。可以看出,第一行有四个孩子,这是因为棋盘上还没有放任何皇后,所以第一行的每个位置都可以放而不会发生冲突。

    第二行就只有三个孩子因为这个时候第一行已经有皇后,要保证不和第一样的发生冲突,选择的余地就变小了。一下情况一次类推。

    如果考虑全部情况的话,应该每个节点都有四个孩子,然后再来判断最终情况是否满足情况,这会坐更多次数的判断,导致低效率。

    上面两种判断法可以导致完全不同的算法效率:

    方法一判断了每一种情况,其实就是穷举法。方法二只判断了一部分情况,对于那些没有判断的情况,是因为我们已经知道他们不可能成为解了,所以就

    没有判断的必要,可以节省大量的时间开支。

    (用稍微专业些的话就是在深度优先遍历解空间的时候每一步都判断当前状态是否满足条件,如不满足就没有就不再继续往下遍历,而是回溯)

    所以思路可以是这样:在第一行放一个皇后(可以是任意位置),然后在第二行找一个可行的位置放置,在这个基础上在第三行找一个没有冲突的位置,如果

    发现某一行没有地方可放了,那么修改它的上一行,(找到另外一个没有冲突的位置),然后在继续遍历。(回溯)

    下面得考虑用什么样的数据结构来存储结果:

    当然,你可以用二维数组来存放,相当于模拟了个棋盘,有皇后的位置存1,没有皇后的位置存0,

    多数的做法是用一个数组来存放,(一维数组),下文会有提到。

    算法实现

    有了上面的分析后,再给出算法应该就能比较好的理解了

    可以用递归和非递归来求解 :

    非递归算法:

    NQueens1(int n)
    { int k, n; extern int x[n]; k=0; x[k]=-1;
    while(k>=0)
    { x[k]=x[k]+1;
    while((x[k]<n) && (!Place(k)) x[k]=x[k]+1;  //如果当前列不满足情况,则判断下一列
    if(x[k]<n)         //如果是上文while中的第二个条件不满足而退出while循环,也就是说找到了可以放置的位置
    { if(k<n-1) { k=k+1; x[k]=-1; }    //判断当前行是不是最后一行,如果是最后一行则表明已经找到结果,打印结果
    else printf(x[0:n-1]);
    }
    else k=k-1;      //上文中while因为第一个条件不满足而退出while循环,在这行里没有满足条件的列,那么退回上一行重新选择满足
                      //条件的的列(回溯)
    }
    }


    代码说明:K表示行数,数组元素X[K]的值表示第K行的皇后位置,比如X[3]=2,表示第3行皇后的位置是2. 

    place()是判断当前位置是否满足条件的函数,如果满足条件返回真,不满足为假。从循环可知,如果当前位置不可行,则判断下一列:X[k]=X[k]+1,由于递归和非递归都会用到这个函数,将在下面提到。

     有人可能注意到X[k]的初始值为-1,是因为对于每一行程序都要从第一个位置(第一列)判断,如果不满足再往后,对于每一列的处理

    是从第一个while循环里开始的,里面的X[k]=X[k]+1,使得第一次判断的是第一列。

    递归算法:

    NQueens2(int k)
    { extern int x[n];
    x[k]=-1;
    while(1)
    { x[k]=x[k]+1;
    while((x[k]<n)&&(!Place(k)) x[k]=x[k]+1;
    if(x[k]<n)
    { if(k<n-1) NQueens2(k+1);      //如果还没找到最终解,则递归调用算法,判断下一行(K+1)
    else printf(x[0:n-1]);
    }
    else return;
    }
    }

    对于两个函数中都用到的place()函数:

    int Place(int k) 
    { int i; extern int x[n];
    for(i=0; i<k-1; i++)
    if( (x[i]==x[k-1]) || (abs(x[i]-x[k-1])==abs(i-k+1)) )
    return false=0;
    return true=1;
    }

    传入参数是K(行数),还引入数组X[n],这样就能知道从第一行到第k行的皇后位置,(上文中说说x[k]这个数组是存放皇后位置的)

    要判断第k行的x[k]列是否满足条件,只要用k前的每一行来和它相比较,只要有其中的一行不满足条件,就返回假。

    (x[i]==x[k-1])表示他们在同一列,(abs(x[i]-x[k-1])==abs(i-k+1)表示他们在同一对角线


    算法实现代码 (递归实现)

    #include<fstream>
    #include<iostream>

    std::ofstream fout("queenoo.txt");

    /**//* 记录当前的放置方案 */
    int *x;
    /**//* 皇后的个数N 和 方案数目 */
    int n,sum=0;
    /**//* 检查参数所指示的这一行皇后放置方案是否满足要求 */
    int Place(int);
    /**//* 递归方法求取皇后放置方案*/
    void Queen1(void);
    /**//* 用户递归求取皇后放置方案的递归方法 */
    void TraceBack(int);
    /**//* 打印当前成功的放置方案 */
    void PrintMethod(void);

    int main()
    {
    using namespace std;
    long start,stop;
    cout<<"input n 输入皇后个数 :";
    cin>>n;

    x=(int *)malloc(sizeof(int)*n);
    time(&start);/**//*记录开始时间*/
    Queen1();
    time(&stop);/**//*记录结束时间*/
    cout<<"一共的方案数为:"<<sum<<"\n";
    cout<<"共花时间:"<<(int(stop-start))<<"\n";
    fout<< "一共的方案数为:"<<sum<<"\n";
    fout<< cout<<"共花时间:"<<((stop-start))<<"\n";

    }

    int Place(int r)
    {
    int i;
    for(i=0;i<r;i++){
    if(x[r]==x[i] || abs(r-i)==abs(x[r]-x[i]))
    return 0;
    }
    return 1;
    }

    void TraceBack(int r)
    {
    int i;
    if(r>=n){
    PrintMethod();
    sum++;
    /**//* PrintMethod(); */
    }else{
    for(i=0;i<n;i++){
    x[r]=i;
    if(Place(r)) TraceBack(r+1);
    }
    }
    }

    void PrintMethod(void)
    {
    int i,j;
    std::cout<<""<<sum<<"个方案\n";
    fout<<""<<sum<<"个方案\n";
    for(i=0;i<n;i++){
    for(j=0;j<n;j++){
    if(j==x[i]) std::cout<<"1",fout<<"1";
    else std::cout<<"0",fout<<"0";
    }
    std::cout<<"\n";
    fout<<"\n";
    }
    }

    void Queen1(void)
    {
    TraceBack(0);
    }

    以上代码cfree5编译通过。

    代码把实验结果输入到文件“queenoo.txt”中,路径就在编译器安装路径中,和执行代码位于同一目录。

    非递归代码类似,就没写出来


    后记

    解决八皇后问题用到的是一种称为回溯的方法,在上面的分析过程中就体现了这种思想,在深度优先遍历解空间的时候加上判断

    条件,只有符合条件的,才继续往下遍历。任何一种理论的产生都是先有实践环节的,只有在实践正确的情况下,才能总结出理论

    而很多算法书都是现给出里论,再给实例,其实这不是一种好的学习方式,这中间学习者少了许多思考、推导的过程,而这过程恰恰

    就是理论的雏形,忽视了这个关键的作用,只是被动的的接受算法思想而不知道算法是怎样产生的,不知其所以然,这样学起来自然就

    不透彻而且很难掌握。

    所以在讲皇后问题的时候,并没有开篇就讲什么是回溯法,回溯法的细想,如何运用回溯法等等。而是用原始的思维去思考解决问题之道,

    等接触回溯法时才恍然,啊、、这就是回溯法。那时候对于理论的学习就能加深对算法思想的理解,自己在加以思考分析,就能比较好的

    掌握算法。


    以上博文如有错误还望各位神仙大牛指正,在下感激不尽


    关于上文中提到的回溯法,会在下一篇博文中有详细说明,欢迎关注


    如有转载请注明出处:http://www.cnblogs.com/yanlingyin/

    一条鱼@博客园 2011-12-19

  • 相关阅读:
    迭代器
    逻辑量词
    How to distinguish between strings in heap or literals?
    Is the “*apply” family really not vectorized?
    power of the test
    The Most Simple Introduction to Hypothesis Testing
    析构函数允许重载吗?
    C++中析构函数的作用
    在C#中的构造函数和解析函数
    c++构造函数与析构函数
  • 原文地址:https://www.cnblogs.com/yanlingyin/p/2292041.html
Copyright © 2020-2023  润新知