数据结构:“八皇后问题”理解回溯算法

回溯算法

算法思想

走一步看一步,走一步判断一步。当出现非法的情况时,算法可以回退到之前的情景,可以是返回一步,有时候可以返回多步,然后再去尝试别的路径和办法。这也就意味着,想要采用回溯算法,就必须保证,每次都有多种尝试的可能。

步骤
  1. 判断当前情况是否非法,如果非法就立即返回;
  2. 当前情况是否已经满足递归结束条件,如果是就将当前结果保存起来并返回;
  3. 当前情况下,遍历所有可能出现的情况并进行下一步的尝试;
  4. 递归完毕后,立即回溯,回溯的方法就是取消前一步进行的尝试。
代码模板
function fn(n) {

    // 第一步:判断输入或者状态是否非法?
    if (input/state is invalid) {
        return;
  }

    // 第二步:判读递归是否应当结束?
    if (match condition) {
        return some value;
  }

    // 遍历所有可能出现的情况
    for (all possible cases) {
  
        // 第三步: 尝试下一步的可能性
        solution.push(case)
        // 递归
        result = fn(m)

        // 第四步:回溯到上一步
        solution.pop(case)
    
    }
    
}

八皇后问题

在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

本题采用回溯法解决。
时间复杂度:O(n!),其中 n 是皇后数量。回溯的过程,其实就是n的一个全排列。

思路分析
  1. 按行遍历,若该行找到符合要求的位置,行+1;
  2. 进入新行后,判断每列是否符合条件。对行,列,对角线进行标记,其中,对角线用到行列之和,行列之差为定值这一性质。
  3. 每次遍历列以后,进行回溯,将标记出来的位置又标记回去。
  4. 使用递归算法(递归算法的·两点在于:半路出家)。
自写代码(清晰,看这个)
void hang(int n)//行数
{
	for(18循环列)
	{
		if(该点符合条件:1.flag[col]=1(true,表示未被标记),即该点不在上一行的皇后(注意,只是上一行的皇后)覆盖的列方向;2.d1和d2等于1,不超过上对角线和下对角线的边界)
		{
			place[n] = col;//第n行第col列摆上了皇后
			flag[col] = false;//此时标记该列
			d1[n-col+7] = false;//标记上对角线
			d2[n+col] = false;//标记下对角线
			if(n<7)//未结束递归摆放下一行的皇后
				hang(n+1)//行+1;
			else
				print();
			//回溯:考虑其他可行方案
			flag[col] = true;
			d1[n-col+7] = true;
			d2[n+col] = true;
		}
	}
}
void print()
{
	cout<<number;//第number个解
	int table[8][8] = {0};//初始化设置表,全部定义为0;
	for(col=0;col<8;col++)
	{
		table[col][place[col]] = 1;
		两个for循环,用i,j打印table;
	}
}
int main()
{
	hang(0);
	return 0;
}
疑虑解答
1. 我不能接受呀!为什么标记出来的位置可以标回去呀?

能提出这样问题的我,显然是没有理解“递归”的魅力。本题用到的回溯法充分展现了递归半路出家的魅力。即:假设我是一个和尚,我离家出走;假设我离家出走,我往某个方向走…如果假设到最后,我能够修成佛。那么,视为成功,打印出这种修佛路线。所以,每一次的”前进“,其实都是假设。

2. 原来上上行标记了该处,撤销的标记把上上行标记的地方也被撤销了怎么办?

好问题,这个问题困扰了我很久。显然,因为我没有冷静的分析。以四皇后的一种情况举例:
我们是先判断出能不能进行,然后把整个八行都遍历完了,再倒着一行一行撤回的标记。(为了打印输出)

在这里插入图片描述

网上的代码实现(不用看)

此代码是百度百科上co的,具体的代码后续补充在评论区。

#include <iostream>
using namespace std;
 
const int N = 8;
int arr[10], total_cnt;
// arr记录每一行(X)皇后的Y坐标
 
bool isPlaceOK(int *a, int n, int c) {
    for (int i = 1; i <= n - 1; ++i) {
        if (a[i] == c || a[i] - i == c - n || a[i] + i == c + n)
            return false;
        //检查位置是否可以放
        //c是将要放置的位置
        //a[i] == c如果放在同一列,false
        //a[i] -+ i = c -+ n 如果在对角线上,false
    }
    return true;
}
 
void printSol(int *a) {
    for (int i = 1; i <= N; ++i) { //遍历每一行
        for (int j = 1; j <= N; ++j) { //遍历每一列
            cout << (a[i] == j ? "X" : "-") << " ";;
        } //如果标记数组中这一行的皇后放在j位置,则输出X,否则输出-,
        //用空格分隔
        cout << endl; //每一行输出一个换行
    }
    cout << endl; //每一组数据一个换行分隔
}
 
void addQueen(int *a, int n) {
    if (n > N) { //n代表从第一行开始放置
        printSol(a);
        total_cnt++;
        return ;
    }
    for (int i = 1; i <= N; ++i) { //i从第1列到第N列遍历
        if (isPlaceOK(a, n, i)) {
            a[n] = i; //如果可以放置,就把皇后放在第n行第i列
            addQueen(a, n + 1);
        }
    }
 
}
 
int main() {
    addQueen(arr, 1);
    cout << "total: " << total_cnt << " solutions.\n";
    return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值