简要说明:
(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) 给定起点和终点,要求找出起点到终点是否存在一条通路。