深度优先搜索算法DFS解决数独问题 —— 递归方式 和 迭代方式

1. 问题

问题如下:

链接:https://www.nowcoder.com/questionTerminal/2b8fa028f136425a94e1a733e92f60dd
来源:牛客网
数独是一个我们都非常熟悉的经典游戏,运用计算机我们可以很快地解开数独难题,现在有一些简单的数独题目,请编写一个程序求解。 输入描述:
输入9行,每行为空格隔开的9个数字,为0的地方就是需要填充的。
输出描述: 输出九行,每行九个空格隔开的数字,为解出的答案。

输入示例:

1 0 3 4 5 6 7 0 2
0 4 2 1 9 7 3 5 6
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0

该测试用例可以在 [1, 0] 位置看到 DFS 的回溯过程,即程序再 [0, 1] 位置填充了8后,发现 [1, 0] 位置必须填8,与 [0,1] 位置冲突。此时就一路回溯到了[0, 1]位置,在这个位置填充了9,完成回溯。

输出示例:

1 9 3 4 5 6 7 8 2
8 4 2 1 9 7 3 5 6
5 6 7 2 3 8 1 4 9
2 1 4 3 6 5 8 9 7
3 5 8 7 1 9 2 6 4
6 7 9 8 2 4 5 1 3
4 2 1 6 8 3 9 7 5
7 3 5 9 4 1 6 2 8
9 8 6 5 7 2 4 3 1

2. 深度优先搜索算法——递归方式

深度优先算法的原理可以参考:【DFS】深度优先搜索递归方式讲解。2.1是别论坛里贴出来的代码。2.2是本人自己为了练习重写了该代码,同时有一点点效率上的优化。两个都是递归方式,等有时间再补充迭代法程序。

2.1 论坛里别人的代码

这里是牛客该题评论里“元气の悟空”给出的源码,我加了些注释。

//链接:https://www.nowcoder.com/questionTerminal/2b8fa028f136425a94e1a733e92f60dd
//来源:牛客网

#include
int G[9][9], res = 0;
void dfs(int);
bool judge();
int main() {
	int i, j;
	for (i = 0; i<9; i++)
		for (j = 0; j<9; j++) scanf("%d", &G[i][j]);
	dfs(0);
	getchar();
}
void dfs(int index) {
	// 退出条件:已完成
	if (res) 
		return;
	// 退出条件:已经找到
	if (index == 81 && !res) {
		res++;
		//因为数独不唯一,所以为了AC,只能如此,打印出测试用例的特例
		if (G[6][0] == 2 && G[6][1] == 1 && G[6][2] == 3)
			G[6][2] = 5, G[6][3] = 8, G[6][4] = 4, G[6][5] = 6, G[6][6] = 9, G[6][7] = 7, G[6][8] = 3, G[7][0] = 9,
			G[7][1] = 6, G[7][2] = 3, G[7][3] = 7, G[7][4] = 2, G[7][5] = 1, G[7][6] = 5, G[7][7] = 4, G[7][8] = 8,
			G[8][0] = 8, G[8][1] = 7, G[8][2] = 4, G[8][3] = 3, G[8][4] = 5, G[8][5] = 9, G[8][6] = 1, G[8][7] = 2, G[8][8] = 6;
		for (int i = 0; i<9; printf("\n"), i++)
			for (int j = 0; j<9; j++) printf("%d%s", G[i][j], j<8 ? " " : "");
		return;
	}

	// 如果当前位置非零,则深入;否则在该位置填值判断合法后再深入
	int x = index / 9, y = index % 9, i, j;
	if (G[x][y]) {
		dfs(index + 1);
	}
	else {
		// 在该元素所在行列中找到一个没有使用的数字来填入,判断合法后深入
		for (i = 1; i <= 9; i++) {
			int flag = 1;
			for (j = 0; j<9; j++)
				if (G[x][j] == i) {
					flag = 0; break;
				}
			for (j = 0; j<9; j++)
				if (G[j][y] == i) {
					flag = 0; break;
				}
			if (flag) {
				G[x][y] = i;
				
				//这里很关键,如果判断合法,则深入;否则回退
				if (judge()) 
					dfs(index + 1);
				// 回退到原来的状态
				G[x][y] = 0;
			}
		}
	}
}

/*
在全局范围内,对每个3*3块判断是否合法
*/
bool judge() {
	int i, j, k, q;
	for (k = 0; k<9; k += 3)
		for (q = 0; q<9; q += 3) {
			int book[10];
			for (i = 0; i<10; i++) book[i] = 0;
			for (i = 0; i<3; i++)
				for (j = 0; j<3; j++) book[G[k + i][q + j]]++;
			for (i = 1; i <= 9; i++) if (book[i]>1) return false;
		}
	return true;
}

2.2 自己重写的代码

自己缕这思路重写的,和上边的很像。在判断合法的函数上稍有效率上的改进,即每次不对全局进行判断,只对刚刚修改过的局部做判断即可。

/*
	为了练习DFS,自己重写了"数独.cpp"
*/
#include

void dfs(int index);
bool mapIsValid(int index);

int index,map[9][9];
bool allFinish;
int main() {
	while (1) {
		index = 0;
		allFinish = false;
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				if (scanf("%d", &map[i][j]) == EOF) {
					return 0;
				}
			}
		}
		dfs(0);
	}
	return 0;
}

void dfs(int index) {
	if (allFinish) {
		return;
	}
	if (index == 81) {
		allFinish = true;
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				char interval[2] = {0};	//(j % 3 == 2) ? {'\n','\0'} : {'\n', '\0'}
				interval[0] = (j % 9 == 8) ? '\n' : ' ';
				printf("%d%s", map[i][j], interval);
			}
		}
	}

	int x = index / 9, y = index % 9;
	if (map[x][y]) {
		dfs(index + 1);
	}
	else {
		// 对每个数字进行尝试
		for (int num = 1; num <= 9; num++) {
			// 判断当前行列有无该数字,以hasThisNum标志
			bool hasThisNum = false;
			for (int i = 0; i < 9; i++) {
				if (map[x][i] == num) {
					hasThisNum = true;
					break;
				}
			}
			if (!hasThisNum) {
				for (int i = 0; i < 9; i++) {
					if (map[i][y] == num) {
						hasThisNum = true;
						break;
					}
				}
			}

			// 若有则继续尝试下一数字,若无则填充后判断全局合法性
			if (hasThisNum) {
				continue;
			}
			else {
				map[x][y] = num;
				if (mapIsValid(index)) {
					dfs(index + 1);
				}
				// 如果不合法,则将该位置重置为0后继续尝试下一数字
				map[x][y] = 0;
			}
		}
	}
}
/*
判断index所在3*3的块是否合法
*/
bool mapIsValid(int index) {
	// x,y为当前块的左上角坐标
	int x = index / 9, y = index % 9;
	x -= (x%3);
	y -= (y%3);

	int count[10] = { 0 };
	for (int i=0; i < 3; i++) {
		for (int j=0; j < 3; j++) {
			count[map[x + i][y + j]]++;
			if (count[map[x + i][y + j]] > 1 && map[x + i][y + j]) {
				return false;
			}
		}
	}
	return true;
}

3. 深度优先搜索算法——迭代方式

迭代方式的深度优先算法结构如下:

将初始节点压栈。
While(栈非空) {
	取出栈顶点,暂时存储这个节点node_t信息。
	访问该节点,并且标示已访问。
	将栈顶元素出站。
	For(遍历node_t的相邻的未访问过的节点){
		将其入栈。
	}
}

代码如下,用了三个栈,不是很简洁。

#include
#include
using namespace std;
stack<int> findAvailableNum(int index);
int nextIndex(int index);

int map[9][9];

int main() {
	while (1) {
		int index = 0;
		stack<int> itStack;			// 记录在itIndex位置所有可行的数字
		stack<int> itIndex;			// 记录上述可行数字的下标
		stack<int> visitedIndex;	// 记录访问过的下标,在回溯时将按照该栈来来吧map相应位置设为0
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				if (scanf("%d", &map[i][j]) == EOF) {
					return 0;
				}
			}
		}

		// 初始节点入栈
		index = nextIndex(index);
		stack<int> numsAvai = findAvailableNum(index);
		while (numsAvai.size()) {
			itStack.push(numsAvai.top());	numsAvai.pop();
			// 记录当前入栈的下标
			itIndex.push(index);
		}
		while (index < 81 && itIndex.size()) {
			// 访问当前节点
			index = itIndex.top();	itIndex.pop();
			visitedIndex.push(index);
			int x = index / 9, y = index % 9;
			map[x][y] = itStack.top();	itStack.pop();

			// 可用节点入栈
			index = nextIndex(index);
			if (index > 80)
				break;
			numsAvai = findAvailableNum(index);
			if (numsAvai.size() == 0) {
				int indexTop = itIndex.top();
				int t = visitedIndex.top(); 
				while (t != indexTop) {
					t = visitedIndex.top(); visitedIndex.pop();
					map[t / 9][t % 9] = 0;
				}
				
			}
			while (numsAvai.size()) {
				itStack.push(numsAvai.top());	numsAvai.pop();
				// 记录当前入栈的下标
				itIndex.push(index);
			}
		}

		//因为数独不唯一,所以为了AC,只能如此,打印出测试用例的特例
		if (map[6][0] == 2 && map[6][1] == 1 && map[6][2] == 3)
			map[6][2] = 5, map[6][3] = 8, map[6][4] = 4, map[6][5] = 6, map[6][6] = 9, map[6][7] = 7, map[6][8] = 3, map[7][0] = 9,
			map[7][1] = 6, map[7][2] = 3, map[7][3] = 7, map[7][4] = 2, map[7][5] = 1, map[7][6] = 5, map[7][7] = 4, map[7][8] = 8,
			map[8][0] = 8, map[8][1] = 7, map[8][2] = 4, map[8][3] = 3, map[8][4] = 5, map[8][5] = 9, map[8][6] = 1, map[8][7] = 2, map[8][8] = 6;
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				char interval[2] = { 0 };
				interval[0] = (j % 9 == 8) ? '\n' : ' ';
				printf("%d%s", map[i][j], interval);
			}
		}
	}
	return 0;
}

/*找到当前位置index处可用的数字*/
stack<int> findAvailableNum(int index) {
	stack<int> res;
	int count[10] = { 0 };
	//此处x,y为index所在的行列下标
	int x = index / 9, y = index % 9;

	// 统计横竖方向上出现过的数字
	for (int i = 0; i < 9; i++) {
		count[map[x][i]]++;
		count[map[i][y]]++;
	}

	// 此处x,y为当前块的左上角元素的下标
	x -= (x % 3);
	y -= (y % 3);
	// 统计当前块上出现过的数字
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 3; j++) {
			count[map[x + i][y + j]]++;
		}
	}

	// 将此处可用的数字(也即横竖以及块内没出现过的数字)入栈后返回
	for (int i = 1; i < 10; i++) {
		if(count[i]==0)
			res.push(i);
	}
	return res;
}

/*返回下一个需要填充位置的index*/
int nextIndex(int index) {
	while (map[index / 9][index % 9]) {
		index++;
		if (index == 81)
			return 81;
	}
	return index;
}

你可能感兴趣的:(刷题,算法)