随笔 - 9  文章 - 0 评论 - 0 阅读 - 53
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

注意:本文只给出作者认为必要的知识点并且可能存在疏漏。


一、题目大意

  在一个 8 × 8 8\times 8 8×8 的棋盘,要在上面放8个皇后,每个皇后的所在行、列和对角线都不能有其他皇后。现在指定其中一个皇后的位置,求所有可能的皇后摆法并按列输出结果。原题链接


二、理论基础

1. 递归

  递归的定义:参见“递归”。 😄
  对于程序来说,递归就是函数自己调用自己。

  抽象一点来说,递归的基本思想就是将难以求解的大问题分解为多个类似且相对容易求解的小问题,并且小问题可以继续分解直到可以直接求值的程度。举个求 4 ! 4! 4! 的例子:

  1. 4 ! 4! 4! 可以拆为 4 × 3 ! 4\times 3! 4×3!此时负责计算4!的步骤正在等待计算3!的结果。
  2. 3 ! 3! 3! 可以拆为 3 × 2 ! 3\times 2! 3×2!等待2!
  3. 2 ! 2! 2! 可以拆为 2 × 1 ! 2\times 1! 2×1!等待1!
  4. 1 ! 1! 1! 可以拆为 1 × 0 ! 1\times 0! 1×0!很明显1!等于1,但严格来说应继续分解。
  5. 0 ! 0! 0! 的值为1。在编写程序时,要注意递归的终止条件,防止死循环。
  6. 从下至上将结果返回到更上层。

核心代码如下:

int fact(int n) {
	return n ? n * fact(n - 1) : 1;
}

2. 解答树

  如果某问题的解可以由多个步骤得到,而每个步骤都有若干种选择(这些候选方案集可能会依赖于先前作出的选择),且可以用递归枚举法实现,则它的工作方式可以用解答树来描述。

  在一个递归问题的求解过程中,将过程中所有部分解和完整解按生成顺序组织起来的一棵树。举个例子,现在有3个数字 1 、 2 、 3 1、2、3 123,将其组成一个3位数且元素不能重复的密码,所有可能密码的解答树如下:
完整解答树
核心代码如下所示,显然递归枚举过程是在生成解答树。枚举:指列出所有可能答案,一一验证是否正确。如果熟悉DFS(参考 DFS-UVA-10562),容易发现解答树正是按DFS遍历顺序生成的

void solve(int *num, int *result, int n, int cur) {
	if (cur == n)//已生成完整解
	{
		for (int i = 0; i < cur; ++i)
			cout << result[i] << type[i == cur - 1];
		break;
	}

	for (int i = 0; i < n; ++i) {
		int pass = 1;
		for (int j = 0; j < cur; ++j) {//判断数字是否重复
			if (result[j] == num[i]) {
				pass = 0;
				break;
			}
		}
		if (pass) {
			result[cur] = num[i];//确定当前位置的值
			solve(num, result, n, cur + 1);//根据部分解继续递归
		}
	}
}

3. 回溯法 本题重点

  当把问题分成若干步骤并递归求解时,如果当前步骤没有合法选择,则函数将返回上一层递归调用,这种现象称为回溯。正是这个原因,递归枚举算法常被称为回溯法,应用十分普遍。
  回溯法的基础是递归枚举,所以回溯法本质还是枚举,但减少了枚举量。

  • 从部分解看,回溯就是在理解了如何构成合法解(最优解)的前提下,当目前部分解为非法(非最优)时,终止当前分支的继续求解。
  • 从程序来看,回溯就是在原递归枚举的代码的基础上(隐式)加入了一段条件返回的代码。
  • 从解答树看,回溯提前停止了不必要分支的递归生成,从而减少了枚举次数。相当于对解答树进行剪枝,剪枝详见:博客待补充

继续解答树的例子,有个要按顺序输入密码的3位密码锁,设正确密码是 2 , 1 , 3 2,1,3 2,1,3,现在有两种方法破解密码:

  1. 你可以暴力枚举出每一个密码,共 3 ! 3! 3! 个,然后一个个试错。如果密码位数很多,可能的密码数将是一个天文数字。
  2. 每输入一位数,就检查是否发生密码错误,如果有,说明以当前数字开头的密码都是错的,换个数继续试。回溯法的解答树如下,与完整解答树相比减少了很多分支。
    回溯解答树

三、解题思路

  每行每列每对角线只能有一个皇后,共8个皇后,最多有 8 ! 8! 8! 种排列方法。
  因为要求按列输出结果,将x,y坐标互换后进行递归(相当于逐行逐列放皇后,检查当前位置如果放皇后是否会与前面的皇后冲突)。使用 x − y x-y xy y − x y-x yx标记从左上角到右下角的对角线,使用 x + y x+y x+y标记从右上角到左下角的对角线。有个皇后的位置已经确定,直接丢弃与其位置不相同的摆法。


四、参考代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define maxn 8
#define INF 0x3f3f3f3f
typedef long long ll;

int result[maxn], vis[maxn], mDia[maxn * 2], sDia[maxn * 2], Case;
int x, y;
void solve(int cur) {
	if (cur == maxn && result[y] == x) {//只输出坐标正确的完整解
		printf("%2d     ", Case++);
		for (int i = 0; i < maxn; ++i)
			printf(" %d", result[i] + 1);
		putchar('\n');
		return;
	}
	for (int i = 0; i < maxn; ++i)
		if (!(vis[i] + mDia[cur - i + 7] + sDia[cur + i])) {//注意保证下标大于-1
			result[cur] = i;
			vis[i] = mDia[cur - i + 7] = sDia[cur + i] = 1;//占用当前行和对应对角线。
			solve(cur + 1);
			vis[i] = mDia[cur - i + 7] = sDia[cur + i] = 0;
		}
}

int main() {
	int T;
	ios::sync_with_stdio();
	cin.tie(0);
	cout.tie(0);

	cin >> T;
	while (T--) {
		memset(vis, 0, 4 * maxn);
		memset(mDia, 0, 8 * maxn);
		memset(sDia, 0, 8 * maxn);
		cin >> x >> y;
		--x, --y;//下标以0开始,按列输出
		Case = 1;
		puts("SOLN       COLUMN");
		puts(" #      1 2 3 4 5 6 7 8\n");
		solve(0);
		if (T)//格式处理
			putchar('\n');
	}

	return 0;
}


  1. 算法竞赛入门经典 [专著] / 刘汝佳编著 ↩︎ ↩︎ ↩︎

posted on   Wayde_CN  阅读(11)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示