经典问题——八皇后问题:最适合C语言初学者的解法

什么是八皇后问题:

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。
八皇后问题
深究过C语言或者其他语言的同学可能有很简单快速的算法,但是对于初学者来说,可以说这是一道比较难的算法题,因为许多算法我们都没有接触过,那对于C语言的初学者我们该怎么来解决这道题呢?接下来我们就一起来探究下解法。

思路分析

首先我们思考一下,我们是应该一个一个的放置皇后还是该先把八个皇后全部放入棋盘然后在进行移动操作?我们是应该一个一个的放置皇后的。所谓牵一发而动全身,当八个皇后全部放入棋盘后,我们每移动一个皇后就会导致其他皇后的位置都得移动,从而导致了运算过程的冗杂重复,也导致了计算时间的增加和内存的浪费,对于追求效率的C语言来说这是不可取的,所以我们选择一个一个的放置皇后。

这样操作有什么优点呢?每当我们在选择移动一个皇后的位置的时候,其它已经放置好了的皇后的位置是不需要移动的,如果当某个皇后在属于它的行都不满足条件时,我们才需要回溯过去改变上一个皇后的位置。我们需要的就是循环的进行回溯操作,直到所有的皇后都放置完成,这样计算机的每一步操作都是有用的,不再是无用功。

我们来看一下回溯法:回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

下面我们一起来看一下八皇后问题的代码实现过程

PS:因为这篇文章针对初学者,所以我们就不用函数来写了,直接在main函数里面实现。

首先是定义声明阶段:

这是一个8*8的棋盘,可能许多同学认为应该定义一个二维数组来保存这个棋盘,但是完全没有必要。
在这里我们只需要定义一个一维数组就完全够用了。为什么呢?因为皇后的攻击力太强,导致同行的皇后都会被攻击,那么一行只能放一个皇后,我们用这个一位数组的值就可以表示这个皇后所在的列了。
声明定义阶段:

	int queen[8] = {0};		//用来储存皇后的位置 即queen的值就为第i行的列
							//queen[0]表示第0行
							//queen[i]表示第i行
	int cnt = 0;			//表示摆放了几个皇后,也表示摆放皇后的行数。
	int col = 0;			//表示在这一列上摆放了皇后
	int sum = 0;			//总共有几种摆法

在定义好之后我们就可以一个一个的放置皇后了
因为我们并不知道总共需要循环多少次,所以我们用一个while(1)的循环来放置皇后。
首先我们要清楚皇后所在的位置:(cnt,rol),在上面的定义阶段我们可以看到用cnt来表示共摆放了几个皇后,用col来表示在这一列上摆放皇后,那当前正在摆放的皇后的坐标不就是**(cnt,col)**吗?

皇后摆放阶段:

首先我们需要判断的是当前这个皇后能否被其他已经放置好了的皇后所攻击到,如果可以被攻击到,那么这个地盘就不属于这个皇后,如果不会被攻击,那大家就相安无事,都安顿下来不要再打打杀杀。
所以我们需要定义一个标志变量isAttack,如果它为1,表示它会被其它皇后所攻击,如果为0则不会被其他皇后攻击。

int isAttack = 0;

然后我们再来判断哪些情况下这个可怜的皇后会被其他同僚所毒杀:
**当然他们不会再同一行上,只可能是在同一列或者同一斜线上。**所以只需要两个if语句就可以把这两种情况都包含了。

		for(i=0;i<cnt;i++){
			if(queen[i] == col){	//表示在同一列上
				isAttack = 1;	
			}	
			int div_row = cnt -i;		//表示斜线上的纵坐标之差
			int div_col = queen[i]-col;		//表示斜线上横坐标之差
			if(div_row == div_col ||div_row == -div_col){ 	//表示在同一斜线上
				isAttack = 1;	
			}
		}

我来解释下代码:

当已经有cnt个皇后被安顿好了,那我们正在寻找安顿之所的楚楚可怜弱小又无助的皇后就得思前想后,和每个皇后都得避嫌,不能让这cnt个皇后抓到把柄,所以需要和每一个皇后都进行比较,如果在这个地方不能安顿,就得另寻住所。所以我们进行了cnt此循环。

前面我们说过,queen[i]它的值表示第i个皇后所在的列,所以我们要用一个if语句来排除同一列的情况,如果在同一列上就把isAttack这个标志位置1;

我们发现当两个皇后处于同一斜线时,她们的纵坐标之差和横坐标之差是相等或者为相反数的

八皇后示例图

比如我们发现Q1和Q2处于同一斜线上,那么她们必然不可能和平共处,此时Q1的坐标是(3.3),而Q2的坐标是(1,5),

此时3-1 = -(3-5),就能证明我们上面所说处于同一斜线上的皇后们她们的横坐标之差和纵坐标之差是相等或者相反的。

所以我们用第二个if语句来排除这种情况,如果处于同一斜线,标志位isAttack置1;

判断完能否被攻击到这个过程后,我们最后需要的是处理当皇后能被攻击或者不能被攻击时的情况。

当皇后不能被攻击即当标志位为0时:

那么很简单,就是把这个皇后安顿下来。记录当前皇后的列数,放置的皇后+1(就是这个皇后经历过重重考验后此刻也暂时变成了一个毒妇人),col置0,让下一个皇后从第0列开始寻找安家之所,重复上面的安家之旅。如果当八个皇后都安顿好之后那恭喜你,这就是一种解法了。只需要打印出来并且寻找下一种解法。我们来看一下代码:

		if(isAttack == 0){	//表示可以放置
			queen[cnt] = col;		//记录皇后当前的列数
			cnt++;					//开始摆放下一个皇后
			col = 0;				//下一个皇后从第一列开始遍历
			if(cnt == 8){			//如果摆满了八个皇后就打印出他们的摆法
				for(i=0;i<8;i++){
					printf("%d  ",queen[i]+1);	
				}	
				printf("\n");	
				sum++;				//并且摆放种数+1
			}
		}

我们该怎么寻找下一种解法了?那就是这题的核心了:回溯算法

do{		//越界问题	//回溯
	cnt--;		//撤回正在摆放的皇后
	col = queen[cnt]+1;		//往下一个列寻找摆放位置
}while(col>=8);			

首先cnt - - ;就是撤回这在摆放的皇后,让她去试一下下一列是否也能安家,如果刚好可以安家,恭喜你,这又是一种解法了,但是当皇后所在的列数超过八列时,我们就需要重新考虑上一个皇后的位置了。

所以我们这里用do{}while();语句,保证当八个皇后都安顿好之后至少撤去一个皇后,让她重新寻找安家之所。然后当皇后在这一行所有八个位置都试过了不能安家之后,即col>=8;就需要麻烦上一个皇后起驾换一个家,让后来的皇后有家可住。比如下图:

八皇后示例
从上图我们可以看出,当上面七个皇后都相安无事安顿好了之后,我们却发现已然没了第八个皇后的容身之所,这是世界人民大团结所绝不能允许的,所以我们需要Q7这个皇后迁居另寻他所(就是Q7往下一列试),我们发现往下一列后Q7也没有了住处,所谓赠人玫瑰手有余香,当你Q7昔日帮助了Q8,今日我Q6忘记爱恨情仇也来助你Q7一臂之力,所以我Q6也一列一列的往后试,如此循环往复,直致所有的皇后们都在这个三千世界找到属于自己的一隅之地。

当标志位为1即当前皇后能被其他皇后攻击到时:

那我们就重复上面的操作,用回溯算法找帮这个皇后找其他安家之所。

else{			//表示不能摆放
	col++;
	while(col>=8){			//回朔
	cnt--;				//退一格
	col = queen[cnt]+1;	//上一个皇后往后移一格
	}
}

什么意思呢?就是让这个皇后的列数col+1,即往后遍历住所,当出现列数超过8的时候就和通过回溯寻找其他方案。
最后通过这些简单地操作计算机就能找出所有的结果了。

while(1)退出条件

我们在一次一次的找寻中,那要怎么样我们才算找到了所有的答案了呢?
这个问题也很简单,只需要一个if语句就搞定:

		if(cnt == 1 && queen[0] == 7 && col == 6){		//表示第一行的皇后已经到了第八列且第二行的皇后到了第六列位置,已经摆放不下皇后了就退出循环
			break;	
		}

什么意思呢?就是当第一行已经有了一个皇后,且这个皇后到了最后一列,而且第二个皇后在遍历到第七列的时候还没有找到合适的位置,那这个时候我们就break退出这个while循环。
退出条件
如上图所示,此时第二个皇后已经不能往后遍历了,而且也已经不能回溯了(第一个皇后已经到底了),那此时我们就退出循环。

完整源代码

最后我们来完整的看一下解决八皇后问题的代码:

#include <stdio.h>

int main(){
	int queen[8] = {0};		//用来储存皇后的位置 即queen的值就为第i行的列
							//queen[0]表示第0行
							//queen[i]表示第i行
	int cnt = 0;			//表示摆放了几个皇后,也表示摆放皇后的行数。
	int col = 0;			//表示在这一列上摆放了皇后
	int sum = 0;			//总共有几种摆法
	while(1){
		//在(cnt,col)这个坐标摆放皇后

		if(cnt == 1 && queen[0] == 7 && col == 6){		//表示第一行的皇后已经到了第八列且第二行的皇后到了第六列位置,已经摆放不下皇后了就退出循环
			break;	
		}
		int isAttack = 0;		//用来表示皇后们之间是否能够攻击的到,如果攻击的到就是1,否则就为0
		int i=0;
		for(i=0;i<cnt;i++){
			if(queen[i] == col){	//表示在同一列上
				isAttack = 1;	
			}	
			int div_row = cnt -i;		//表示斜线上的纵坐标之差
			int div_col = queen[i]-col;		//表示斜线上横坐标之差
			if(div_row == div_col ||div_row == -div_col){ 	//表示在同一斜线上
				isAttack = 1;	
			}
		}
		if(isAttack == 0){	//表示可以放置
			queen[cnt] = col;		//记录皇后当前的列数
			cnt++;					//开始摆放下一个皇后
			col = 0;				//下一个皇后从第一列开始遍历
			if(cnt == 8){			//如果摆满了八个皇后就打印出他们的摆法
				for(i=0;i<8;i++){
					printf("%d  ",queen[i]+1);	
				}	
				printf("\n");	
				sum++;				//并且摆放种数+1
				do{		//越界问题	//回朔
					cnt--;		//撤回正在摆放的皇后
					col = queen[cnt]+1;		//往下一个列寻找摆放位置
				}while(col>=8);			
			}
		}else{			//表示不能摆放
			col++;
			while(col>=8){			//回朔
				cnt--;				//退一格
				col = queen[cnt]+1;	//上一个皇后往后移一格
			}
		}
	}
	printf("总共有%d种摆法\n",sum);
	return 0;	
}

结果:

结果1
结果2
结果

完美解决!!

用最简单的代码实现最复杂的问题,C语言并不是很难。

发布了14 篇原创文章 · 获赞 84 · 访问量 2800

猜你喜欢

转载自blog.csdn.net/weixin_42617375/article/details/102983196