1. 程式人生 > 其它 >【dawn·資料結構】解數獨問題(C++)

【dawn·資料結構】解數獨問題(C++)

技術標籤:資料結構(C++)資料結構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) 給定起點和終點,要求找出起點到終點是否存在一條通路。