【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的格子,因此可以聯想到二維陣列(矩陣)的儲存形式。
(3) 那麼在回溯的過程中需要區分,當前格子是原先輸入時的空白格(對應字元“.”)還是已有的“固定格”。如果是後者,在回溯的過程中不能改變。如果是前者,才是我們一一試探的物件。因此在儲存數獨每個格子時,需要增加一個標記,來區分這兩者。
(4) 在每一次試探時需要記憶上一步執行到的數字,然後緊接著進行試探。可以直接通過查詢獲取,我的思路里是放在了一個棧內,棧的資料結構實在是再適合回溯法不過了。(儘管這樣浪費了一定空間,但也簡化了一些程式碼)
(5) 設想試探時的步驟。需要根據記憶確定本次需要試探的值,並且根據數獨的規則檢查這個值是否合法。如果不合法繼續進1,如果合法可以繼續試探下一格。當一格試探過了9之後,表明需要繼續回溯試探前一格的下一個可能的值,以此類推。
(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) 給定起點和終點,要求找出起點到終點是否存在一條通路。