【dawn·数据结构】解数独问题(C++)

简要说明:
(1)题目来源网络(题目要求和输入样例参考LeetCode相同题目)
链接:https://leetcode-cn.com/problems/sudoku-solver/
(2)由于作者水平限制和时间限制,代码本身可能仍有一些瑕疵,仍有改进的空间。也欢迎大家一起来讨论。
——一个大二刚接触《数据结构》课程的菜鸡留

题目简介

编写一个程序,给定部分格子及数字,通过填充空格来完成数独。对于数独有如下要求:

1、数字1-9在每一行只出现一次。
2、数字1-9在每一列只出现一次。
3、数字1-9在所属九宫格(粗实线分隔的3×3宫格)只出现一次。

输入样例要求:

1、需要输入9行字符串,每一行字符串长度为9,依次代表当行的9格。
2、用字符“.”代替空白格。
3、这样表示的数独永远是9×9的。
4、可以假设数独只有唯一解。

参考输入样例:

好像直接输入文字会被奇奇怪怪地修正格式

参考输出样例:

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

思路分析

注:仅代表个人思路。

(1) 首先要确定数独的储存形式。由于是9×9的格子,因此可以联想到二维数组(矩阵)的储存形式。
(2) 由于数独的解决过程实则是一个不断试探的过程,因此类似于笔者在《数据结构》课程中所涉及的“迷宫问题”(可参见补充部分)的解法,思路应是回溯法。
(3) 那么在回溯的过程中需要区分,当前格子是原先输入时的空白格(对应字符“.”)还是已有的“固定格”。如果是后者,在回溯的过程中不能改变。如果是前者,才是我们一一试探的对象。因此在储存数独每个格子时,需要增加一个标记,来区分这两者。
(4) 在每一次试探时需要记忆上一步执行到的数字,然后紧接着进行试探。可以直接通过查找获取,我的思路里是放在了一个栈内,栈的数据结构实在是再适合回溯法不过了。(尽管这样浪费了一定空间,但也简化了一些代码)
(5) 设想试探时的步骤。需要根据记忆确定本次需要试探的值,并且根据数独的规则检查这个值是否合法。如果不合法继续进1,如果合法可以继续试探下一格。当一格试探过了9之后,表明需要继续回溯试探前一格的下一个可能的值,以此类推。
(6) 那么就要确定这个检查机制应该在何时实现。如何实现是一个不复杂的问题。我的方法是在指向目标格子、进行任何处理前先来检查各个位置是否可行,然后再来根据反馈的结果确认下一个试探的值应是多少。也可以在尝试试探每一个值时都尝试一次这个值是否合法,但在有连续多个不合法的值试探时,后者可能会花费更多的时间。
(7) 确认试探的顺序,使用朴素的行优先原则。
(8) 结合之前的思路,我的想法是在储存格子时定义一个新的结构(struct),既存放具体的值(在内存足够的前提下,定义为int型),又存放是否为给定格子的标记(bool型)。同时由于每一个格子的行、列、九宫格的已有值决定了部分的数据是不合法的,而且这也是一个格子的属性,因此在不顾虑内存的前提下同时储存一个bool数组,长度为9,依次对应1~9是否允许被放在本格。

代码部分

#include <iostream>
#include <fstream>
#include <string>
#include <stack>
//程序中stack类的一些成员函数可能与默认有所不同,具体情况将进行说明。
using namespace std;

typedef struct sudokunode {
	//构造函数
	sudokunode():_data_(0),_isconst_(false) {
		for (int i=0;i<9;i++)  _position_[i]=true;
	}
	
	//成员变量
	int _data_;
	bool _position_[9];
	bool _isconst_;
}SN;

struct sudoku {
	//访问第i行第j列的元素(实际上是为了代码直观一些)
	int askData(int i, int j) const  {  return _m_[9*i+j]._data_;  }
	//查找给定元素的_position_数组的某一项(即判断某个值是否可以放在该格)
	bool askPosition(int i, int j, int po) const  {  return _m_[9*i+j]._position_[po];  }
	//修改data
	void changeData(int i, int j, int d)  {  _m_[9*i+j]._data_=d;  }
	//修改data, 同时标记这一格是“常量格”(即在输入时即被确定的格子)
	void changeData(int i, int j, int d, bool c)  {  changeData(i,j,d); _m_[9*i+j]._isconst_=true;  }
	//修改_position_数组的某一项
	void changePosition(int i, int j, int po, bool b)  {  _m_[9*i+j]._position_[po]=b;  }
	//对该格重新修改_position_数组
	void rePosition(int i, int j);
	//判断该格是否在输入时就被确定下来(即所说的“常量格”)
	bool isConst(int i, int j) const  {  return _m_[9*i+j]._isconst_;  }
	//按照输出样例, 输出这个数独
	void print();
	
	//成员变量
	SN _m_[81];  //对应9×9
};

void sudoku::rePosition(int i, int j) {
	for (int n=0;n<9;n++)  changePosition(i,j,n,true);  //最开始都定义为true.
	int rb,cb;
	int temp;
	//考察同行
	for (int n=0;n<9;n++)
		if (n!=j&&(temp=askData(i,n))>0)  changePosition(i,j,temp-1,false);
	//考察同列 
	for (int n=0;n<9;n++)
		if (n!=i&&(temp=askData(n,j))>0)  changePosition(i,j,temp-1,false);
	//考察所处九宫格
	rb=(i/3)*3;
	cb=(j/3)*3;
	for (int m=rb;m<rb+3;m++)
		for (int n=cb;n<cb+3;n++)
			if (m!=i&&n!=j&&(temp=askData(m,n))>0)  changePosition(i,j,temp-1,false);
}

void sudoku::print() {
	for (int i=0;i<9;i++) {
		for (int j=0;j<9;j++)  cout<<askData(i,j)<<' ';
		cout<<endl; 
	}
	cout<<endl;
}

void solution(sudoku& s) {
	int i=0,j=0,repo=0;
	stack<int> st;  //用于储存回退后的试探数从哪个数开始
	//先找到第一个可以修改的格. 假设输入不是完整的九宫格
	while (s.isConst(i,j)) {  //跳过“常量格”
		j++;
		if (j==9) {
			i++;
			j=0;
		}
	}
	//先考察这第一个可以修改的格子
	s.rePosition(i,j);
	while (repo<9&&!s.askPosition(i,j,repo))  ++repo;
	if (repo==9)  s.fail();
	s.changeData(i,j,repo+1);  //repo对应数组的下标, 对应的值需要加1. 后同
	st.push(repo);
	repo=-1;  //while循环中试探的第一步会自增repo, 因此初始化为-1
	while (1) {  //按照行优先原则试探数独问题
		if (i==9) {  //试探超出范围, 这表明已试探过的所有格都合法, 即一种解
			s.print();
			//输出完之后回退到前一格进行试探
			i=j=8;
			while (s.isConst(i,j)) {  //找到前一个非“常量格”
				--j;
				if (j<0) {
					--i;
					j=8;
				}
				if (i<0) {  //理论上不可能所有格子值都是给定的
					return;
				}
			}
			st.pop(&repo);  //模板语法下:void stack<T>::pop(T* address);  即将栈顶元素放入*repo, 同时出栈
		} 
		if (!s.isConst(i,j)) {  //非“常量格”, 可以开始更改. 否则直接找后继格 
			s.rePosition(i,j);
			++repo;
			while (repo<9&&!s.askPosition(i,j,repo))  ++repo;  //找到下一个合法数字
			if (repo==9) {  //此时说明已经无解, 需要回退到最后一个可修改格子, 并修正repo 
				s.changeData(i,j,0);  //修正当前data为初始值 
				--j;
				if (j<0) {
					--i;
					j=8;
				}
				if (i<0) {  //此时说明无解, 可以返回 
					return;
				}
				while (s.isConst(i,j)) {  //恰在前面的格子可能是“常量格”, 先回退一次后前找“非常量格”
					--j;
					if (j<0) {
						--i;
						j=8;
					}
					if (i<0) {  //此时说明无解, 可以返回 
						return; 
					}
				}
				st.pop(&repo);  //修正repo
				continue; 
			}
			else {  //说明这个值是可以放入的, 那么放入stack并进入下一个格子. 
				s.changeData(i,j,repo+1);
				st.push(repo);
				repo=-1;
			}
		}
		//修改指针 
		++j;
		if (j==9) {  //行尾, 更改指针. 
			i++;
			j=0;
		}
	}
}

int main() {
	//输入数独
	sudoku s;
	//这里为了方便提供文件读写, 搭配getline()函数
	ifstream ifs;
	string ifr;
	ifs.open("Sudoku.txt");
	for (int i=0;i<9;i++) {
		getline(ifs, ifr);
		for (int j=0;j<9;j++) {
			if (ifr[j]=='.')  s.changeData(i,j,0);
			else  s.changeData(i,j,ifr[j]-'0',0);  //注意确认“常量格”时调用的changedata函数是第二个(即多一个bool参数的)
		}
	}
	solution(s);
}

改进空间

也如之前探讨的,这个程序仍然会有一些改进的空间:
(1) 格的储存形式是否可以简化,例如分别定义int型、bool型二维数组,同时试探时共享一个bool[9]。这样可以节省更多的储存空间。
(2) 有一些判断仍然可能是多余的。
(3) 一些公共步骤(如找后继格、前驱格)可以放在一个函数中,即可以简化代码的长度。
(4) 思路仍然有些复杂,或有更高效的试探方法。

补充部分

(1) 本代码提供的方法可以同时print出多种解法,题目中提及可以只输出一种(即假定数独只有唯一解)。在这种假设下,代码的对应修改可以集中在更改while循环的条件判断以及循环体的头几步中,可自行实现。
(2) 迷宫问题的简要描述:(读者可自行实现,简化版可以参考这里

(1) 一个迷宫可以用二维数组maze[m+2][n+2]表示,其中第0行、第m+1行、第0列、第n+1列表示迷宫的围墙。
(2) maze[i][j] (0<i<m+2, 0<j<n+2) 为1表示该位置是墙壁,无法通过;为0表示该位置是通路。所有围墙除两个位置外都为1,两个为0的位置分别是起点和终点。
(3) 你的前进方向可以有八种,分别是正北、正南、正西、正东、东北、东南、西北、西南(不妨设从下标大指向下标小的位置为北)。
(4) 给定起点和终点,要求找出起点到终点是否存在一条通路。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值