以一个初学者的视角理解回溯问题——简单易懂的N皇后问题解决方案

问题描述:

N皇后问题是指在N*N的国际象棋棋盘上放上N个皇后,她们之间互相不能攻击,用回溯算法得出这个问题的所有解。

解决思路:

1、理解回溯算法:

作为五大经典算法,回溯算法的地位之高不言而喻,在面对需要一步一步解决的问题时(例如:下棋、迷宫、最佳调度),它是一种非常通用的解题方法。这种算法概括起来就是一种类似枚举的搜索尝试过程,它的基本思想就是在空间中摸索,遇到不满足约束条件时,回退到之前导致这个结果的节点并另做选择,直到搜索出结果为止。

2、关于错误的理解

     回溯问题非常容易地被错误的理解为这种感觉:

1、走一步

2、如果这步对:继续走

3、否则:返回上一步,选择另一步

     这里“步”的概念非常容易被混淆,回溯问题追溯的是“步”,但不是“一步”,而是一系列的步数(一组选择),就说如果你如果做错了什么事情,你应该回到的是你开始做这件事情的时候而不是前一步),并且在那里选择另外一条路去尝试。

       这听起来有些匪夷所思,但这就是我们来解决这个问题思想的核心所在。

       我们来看一段代码,看看我们如何实现这个思想。

bool Walk-1(走的坐标)//第一次递归 
{
    if(判断还能不能走的函数)
    {
         return 不能走; //退出递归基准情况 1 
	}
	if(走到终点了)
	{
		return 到终点!//退出递归基准情况 2 
	}
	if(Walk(下一步))//看看下步能不能走 
	{
		return 能走;
	}
	if(walk(另一步))
	{
	     return 能走; 
	} 
}
bool Walk-2(走的坐标)//第二次递归 
{
    if(判断还能不能走的函数)
    {
         return 不能走; //退出递归基准情况 1 
	}
	if(走到终点了)
	{
		return 到终点!//退出递归基准情况 2 
	}
	if(Walk(下一步))//看看下步能不能走 
	{
		return 能走;
	}
	if(walk(另一步))
	{
	     return 能走; 
	} 
}
.......
bool Walk-n(走的坐标)//第n次递归 
{
    if(判断还能不能走的函数)//如果这里判断为不能走,则上一个递归的walk n-1会得到“不能走”,上上个递归也会得到“不能走”
    {
         return 不能走; //于是一层一层的walk全都返回“不能走”,最后回溯到walk-1,于是walk-1就不走“下一步”了,它就会去尝试走"另一步" 
	}
	if(走到终点了)
	{
		return 到终点!//退出递归基准情况 2 
	}
	if(Walk(下一步))//再看看下步能不能走
	{
		return 能走;
	}
	if(Walk(另一步))
	{
	     return 能走; 
	} 
}

       这里我是用递归的办法来实现这个算法。这是一种非常特殊的递归,它在if语句中调用走下一步递归函数,这里的if(Walk(下一步))就好像可以返回未来的结果,并且通过这个结果来改变当前一步的进程,真正做到了你如果做错了什么事情,你就会回到开始做这件事情的时候。

“if(递归函数(下一步))”这个可以作为回溯问题的一个模板。(想回溯问题想不通的时候感觉就往这个里面套就好了^_^)

所以我们现在要解决N皇后问题,就明确了回溯算法的解决思路:

第一步:放下皇后

第二步:判断是否可以放,如果可以放就到第三步,否则就第四步。

第三步:判断是否放到最后一行,如果不是,递归进入下一行,回到第一步;如果是,打印一个解,返回失败,递归返回之前的节点。

第四步:判断是否放到最后一列,如果不是,递归进入下一列,回到第一步;如果是,就返回失败,递归回到上一个节点。

第五步:判断是否所有的可能性都下完,如果是,返回成功,递归结束;如果否,什么都不做。

#include<iostream>
#define NQ 7 //皇后的数量 
using namespace std;
char NQueen[NQ][NQ];//皇后的二维数组
int Index = 0;//数打出多少个 
bool judge(int row,int column)//判断皇后是否互相攻击 
{
    if(row==0)//下第一步肯定没人攻击
        return true;
    for(int i = 0;i < NQ;i++)//判断斜对角攻击
    {
        for(int j = 0;j < NQ;j++)
        {
            if(!(row==i&&column==j))
                if(row+column==i+j||row-column==i-j||column-row==j-i)
                    if(NQueen[i][j]=='Q')
                        return false;
        }
    }
    for(int i = 0;i < row;i++)//判断列攻击 
    {
        if(NQueen[i][column]=='Q')
            return false;
    }
    for(int i = 0;i < column;i++)//判断行攻击(其实没必要) 
    {
        if(NQueen[row][i]=='Q')
            return false;
    }
    return true;
}
void print()//打印函数
{
    for (int i = 0;i<NQ;i++)
    {
        for (int j = 0;j < NQ;j++)
        {
            cout<<NQueen[i][j];
        }
        cout<<endl;
    }
}
bool WalkNQ(int row,int column)//以皇后落子的位置作为参数
{
    if(row == NQ)//基准情况1、当行数不断增加到达最大值时 
    {
        Index++;//计数君增加 
        cout<<Index<<endl;//打印计数君 
        print();//打印地图 
        cout<<endl;
        return false;//若只需要一种情况,此处return true即可
		//到底继续返回失败可以使程序不断回溯,直至最后一种情况 
    }
    if(column== NQ)//基准情况2、若列数不断增加大于棋盘范围(意为无子可下) 
    {
        return false;//返回失败 
    }
    NQueen[row][column] = 'Q';//放下一个皇后 
    if(judge(row,column))//如果判断可以走 
    {
        if(WalkNQ(row+1,0))//试探下一行能不能走(核心!!) (从第一列开始下)
        {
            return true;
        }
    }
    NQueen[row][column] = '*';//如果不能走,放下的Q变成棋盘 
    return WalkNQ(row,column+1);//走下一列试试看 
}
int main()
{
    for (auto &i : NQueen)//给全部的NQueen赋值 (C++11新特性)
    {
        for (auto &j : i)
        {
            j = '*';
        }
    }
    WalkNQ(0,0);//初始位置 
    return 0;
}

整个基本流程都在代码上打好注释了,看代码即可。

要提的是此处的递归函数WalkNQ的基准情况(Base Condition)设置相当巧妙,在得到一个解的时候(第一个基准情况),不返回true,而是打印该时刻的数组,直到回溯到最后一种情况的时候才结束递归。这种设置多种基准情况的形式可以使回溯算法穷举结果。这个确实有点难理解,但是理解透彻了做很多题会轻松很多。


3、判断皇后互相攻击的函数

解决完回溯算法的函数,我们最后来看看判断皇后是否互相攻击的函数

判断同行同列攻击就不用说了,判断斜对角的攻击比较复杂。

这里有个挺有意思的规律:

凡是在斜对角上的坐标,行列相加或相减都会等于皇后所在位置的行列相加或交错相减,这个很拗口,我把图和代码放上来。


for(int i = 0;i < NQ;i++)//判断斜对角攻击
    {
        for(int j = 0;j < NQ;j++)
        {
            if(!(row==i&&column==j))//小心!他自己本身的位置也是满足判断是否被攻击的公式的!要剔除他本身的位置。
                if(row+column==i+j||row-column==i-j||column-row==j-i)
                    if(NQueen[i][j]=='Q')/
                        return false;
        }
    }

解决了这个问题,基本上整个N皇后问题就迎刃而解了!


感觉这种解决问题的思路同样适用于最佳调度和迷宫问题,下次可以再写一篇。

代码写得还蛮OK,改改添添应该可以弄出很多玩法,欢迎调戏!

水平尚不足,欢迎指正批评!

猜你喜欢

转载自blog.csdn.net/narcissus2_/article/details/80445663