求解數獨

janbar發表於2020-10-28

前言

數獨這個遊戲很適合鍛鍊大腦思考,由於規則很簡單,因此很適合我寫程式碼拿來破解。所以就有了這篇隨筆了。
首先我想通過自己的思考完成數獨的求解,然後再到網上抄答案。提供一個【線上玩數獨】的網站。

我的程式碼

程式碼講解

    我想通過自己的思路來求解,雖然網上肯定有非常巧妙高效的解法。因此我安裝了HoDoKu這個軟體,這個軟體會分析當前數獨每個待填格子可能存在的值,目前我發現Naked Or Hiden Single這2中是最容易找出來的,找出來了該位置就必填那個數。下圖是一個例子,表示裸露的單個數字,該位置只有一種可能值。經過仔細研究,我得出了2個原則:

  1. 當前位置只有一種可能值,則優先填入。
  2. 當前位置的可能值在當前行列宮格唯一,那麼這個值是隱藏的單個,也是必填的。

    有了上述2個原則,那麼我必須有一種演算法計算每一個待填單元格可能填入的資料。其實很簡單,只需要遍歷這些代填的位置,然後變數當前行列所在宮格,去掉已經確定的值,剩下的就是代填值。
    經過上面的計算也只能將代填位置確認值填好,但是剩下有可能存在多個值且無法確定。因此我首先想到的就是暴力破解法,假設代填位置為其中一個可能值,由此繼續填數字,每次填入數字後再進行一次上面找以確定單個數,如果無法繼續,或者得到一個存在某個位置沒有可能填入資料則說明假設出錯,恢復上一次儲存的狀態,繼續假設下一個可能值。具體流程圖如下:
    下面就貼上我的程式碼,其中儲存狀態用了棧結構,每次快取則壓棧,恢復則彈棧:

package main
 
import (
	"container/list"
	"fmt"
	"log"
 
	"time"
 
	"io/ioutil"
 
	"flag"
 
	"github.com/jan-bar/golibs"
)
 
const Length = 9 /* 數獨長寬都是9 */
 
/**
* 下面這個結構有點複雜
* num:  當前位置資料,包括初始值,已經填寫的值
* cnt:  標識該位置可能數的個數
* flag: 初始時和num相同,只是在結果列印時區別初始值和計算得到值顏色
* may:  該陣列記錄當前位置可能值,總是從陣列頭開始
**/
type MySudokuData struct {
	num, cnt, flag int         /* 點位具體值,可能值的個數,該位置需要填值 */
	may            [Length]int /* 記錄點位可能的值 */
}
 
/**
* 下面結構儲存存在多個可能值的位置
* pos:  記錄可能值的座標(其中i表示多少行,j表示多少列)
* cnt:  記錄這些座標個數
**/
type MyMayPos struct {
	pos [Length * Length]struct {
		i, j int /* 快取待定位置i,j值 */
	}
	cnt int /* 待定位置個數 */
}
 
/**
* 總體的資料結構
* data:  記錄9*9的81個點位資料
* pos:   表示可能值的資料
* dot:   在計算時表示當前假設到哪個可能點
* may:   在計算時表示dot的點找到哪個可能值
**/
type MyCacheData struct {
	data     [Length][Length]MySudokuData /* 快取整個數獨 */
	pos      MyMayPos                     /* 快取當前可能位置 */
	dot, may int                          /* 快取第幾個可能點,和該點第幾個可能值 */
}
 
var SudokuData MyCacheData /* 得到數獨資料,和每個空位可能值,用於計算 */
 
func init() {
	fr := flag.String("f", "Sudoku.txt", "input data file!")
	flag.Parse()
 
	byt, err := ioutil.ReadFile(*fr)
	if err != nil {
		log.Fatal(err.Error())
	}
 
	var i, j, cnt, tmp int
	for _, v := range byt {
		if tmp = int(v - '0'); tmp >= 0 && tmp <= 9 { /* 只處理檔案中數字0~9 */
			SudokuData.data[i][j].num = tmp
			SudokuData.data[i][j].flag = tmp
 
			if cnt++; j < 8 {
				j++
			} else {
				i++
				j = 0
			}
		}
	}
 
	if cnt != 81 { /* 無論如何必須要有81個輸入 */
		log.Fatal("輸入檔案不正確!")
	}
}
 
/**
* 主程式入口
* http://aperiodic.net/phil/scala/s-99/
**/
func main() {
	var (
		pos, may, x, y, cnt int
		CacheData           = list.New()  /* 快取資料棧 */
		TmpElement          *list.Element /* 快取連結串列元素 */
		tStart              = time.Now()  /* 開始時間 */
	)
 
	FlushMayNum()                 /* 初始重新整理一下可能值 */
	for false == GameComplete() { /* 如果沒有完成則一直繼續計算 */
		for ; pos < SudokuData.pos.cnt; pos++ { /* 遍歷可能點 */
			x, y = SudokuData.pos.pos[pos].i, SudokuData.pos.pos[pos].j
			for ; may < SudokuData.data[x][y].cnt; may++ { /* 遍歷可能點中可能填寫的值 */
				SudokuData.dot, SudokuData.may = pos, may
				CacheData.PushFront(SudokuData) /* 儲存當前狀態到棧中 */
 
				SudokuData.data[x][y].num = SudokuData.data[x][y].may[may] /* 資料中填寫可能值 */
				cnt++
				if FlushMayNum() { /* 進行一次尋找,返回true表示還能繼續找 */
					pos, may = 0, 0
					goto NextGameLoop /* 資料已經重排,所以要重新遍歷 */
				} /* 下面是else部分 */
 
				/* 如果找到了一個沒有可能值的位置,從棧頂取資料,從下一個值開始遍歷 */
				if TmpElement = CacheData.Front(); TmpElement == nil { /* 取棧頂元素,計算下一個可能值 */
					return /* 棧中沒有資料,無解 */
				}
				SudokuData = TmpElement.Value.(MyCacheData) /* 恢復上次狀態 */
				CacheData.Remove(TmpElement)                /* 移除棧頂狀態 */
			}
		}
 
		/* 下面表示通過上面的計算,把所有可能點的可能值遍歷,還是無法得到結果 */
		if TmpElement = CacheData.Front(); TmpElement == nil { /* 取棧頂元素,計算下一個可能值 */
			return /* 棧中沒有資料,無解 */
		}
			SudokuData = TmpElement.Value.(MyCacheData) /* 恢復上次狀態 */
			CacheData.Remove(TmpElement)                /* 移除棧頂狀態 */
			pos, may = SudokuData.dot, SudokuData.may+1 /* may從下一個開始 */
	NextGameLoop: /* 重排的資料繼續計算 */
	}
 
	fmt.Println("計算耗時 :", time.Since(tStart))
	PrintSudoku() /* 完成後列印數獨 */
	fmt.Scanln()  /* 避免一閃而逝 */
}
 
/**
* x橫座標,向下遞增
* y縱座標,向右遞增
* 如果執行過程中有空位只有唯一值,那麼填好值,再重新整理一次
* 該方法結束後,空位一定存在多個可能值
* 返回false表示有位置無解,返回true表示所有位置都有多個解
**/
func FlushMayNum() bool {
	var i, j, k, t, x, y, tmpMay, flagBreak, xS, xE, yS, yE int
 
StartLoop: /* 如果結果中有唯一值的位置,則重新計算 */
	SudokuData.pos.cnt = 0 /* 待定位置從0計數 */
	for i = 0; i < Length; i++ {
		for j = 0; j < Length; j++ {
			if 0 == SudokuData.data[i][j].num { /* 空位才需要重新整理可能值 */
				for k = 0; k < Length; k++ {
					SudokuData.data[i][j].may[k] = k + 1 /* 為可能值賦初值 */
				} /* 初始i,j位置預設可能存在的數值 */
 
				for k = 0; k < Length; k++ {
					if t = SudokuData.data[i][k].num; t > 0 { /* 遍歷行 */
						SudokuData.data[i][j].may[t-1] = 0 /* 從可能中剔除該數字 */
					}
					if t = SudokuData.data[k][j].num; t > 0 { /* 遍歷列 */
						SudokuData.data[i][j].may[t-1] = 0 /* 從可能中剔除該數字 */
					}
				} /* 上面迴圈剔除行列的值 */
 
				xS = i / 3 * 3 /* 所在宮格x起始 */
				xE = xS + 3    /* 所在宮格x結束 */
				yS = j / 3 * 3 /* 所在宮格y起始 */
				yE = yS + 3    /* 所在宮格y結束 */
				for ; xS < xE; xS++ {
					for k = yS; k < yE; k++ {
						if t = SudokuData.data[xS][k].num; t > 0 {
							SudokuData.data[i][j].may[t-1] = 0 /* 從可能中剔除該數字 */
						}
					}
				} /* 上面雙層迴圈遍歷所在宮格 */
 
				/* 下面將可用值左移,保證有效值從陣列頭開始 */
				for k, SudokuData.data[i][j].cnt = 0, 0; k < Length; k++ {
					if t = SudokuData.data[i][j].may[k]; t > 0 {
						SudokuData.data[i][j].may[SudokuData.data[i][j].cnt] = t
						SudokuData.data[i][j].cnt++ /* 將可能的值移動到前面 */
					}
				}
 
				if 0 == SudokuData.data[i][j].cnt {
					return false /* 該位置沒有解 */
				}
 
				if 1 == SudokuData.data[i][j].cnt { /* 如果當前位置只有一種可能值 */
					SudokuData.data[i][j].num = SudokuData.data[i][j].may[0] /* 將該值填入陣列中 */
					goto StartLoop                                           /* 重新重新整理可能值資料 */
				}
 
				/* 下面用插入排序發將每個點可能的個數從小到大新增到MayPos中 */
				//for k = 0; k < SudokuData.pos.cnt; k++ {
				//	if SudokuData.data[i][j].cnt < SudokuData.data[SudokuData.pos.pos[k].i][SudokuData.pos.pos[k].j].cnt {
				//		break /* 找到位置,由小到達的排序,可以讓迴圈次數減少 */
				//	}
				//}
				//for t = SudokuData.pos.cnt; t > k; t-- { /* 上面找到位置,該位置右邊資料集體右移一位 */
				//	SudokuData.pos.pos[t].i, SudokuData.pos.pos[t].j = SudokuData.pos.pos[t-1].i, SudokuData.pos.pos[t-1].j
				//}
				//SudokuData.pos.pos[k].i, SudokuData.pos.pos[k].j = i, j
				//SudokuData.pos.cnt++ /* 可能點個數加1 */
				SudokuData.pos.pos[SudokuData.pos.cnt].i, SudokuData.pos.pos[SudokuData.pos.cnt].j = i, j
				SudokuData.pos.cnt++ /* 可能點個數加1 */
			} /* end if 0 == SudokuData[i][j].num { */
		} /* end j */
	} /* end i */
 
	flagBreak = 0
	/* 上面得到一個局面,及可能點一定有多個值,下面找隱藏的只有一個解的位置 */
	for i = 0; i < SudokuData.pos.cnt; i++ { /* 遍歷每個可能點位置 */
		x, y = SudokuData.pos.pos[i].i, SudokuData.pos.pos[i].j /* 得到該點位置 */
		for j = 0; j < SudokuData.data[x][y].cnt; j++ {
			tmpMay = SudokuData.data[x][y].may[j] /* 找這個可能值,看看是否為隱藏單個 */
 
			for k = 0; k < Length; k++ {
				if t = SudokuData.data[x][k].num; t == 0 { /* 遍歷行中不確定格子 */
					for ; t < SudokuData.data[x][k].cnt; t++ {
						if tmpMay == SudokuData.data[x][k].may[t] {
							goto NextFlagX /* 這個可能值和在當前行不唯一 */
						}
					}
				}
			} /* 在行上找相同可能值 */
			SudokuData.data[x][y].num = tmpMay /* 這個值在行上可能值中是唯一,填值並重新填值 */
			flagBreak = 1
			break
 
		NextFlagX:
			for k = 0; k < Length; k++ {
				if t = SudokuData.data[k][y].num; t == 0 { /* 遍歷列中不確定格子 */
					for ; t < SudokuData.data[k][y].cnt; t++ {
						if tmpMay == SudokuData.data[k][y].may[t] {
							goto NextFlagY /* 這個可能值和在當前列不唯一 */
						}
					}
				}
			} /* 在列上找相同可能值 */
			SudokuData.data[x][y].num = tmpMay /* 這個值在行上可能值中是唯一,填值並重新填值 */
			flagBreak = 1
			break
 
		NextFlagY:
			xS = x / 3 * 3 /* 所在宮格x起始 */
			xE = xS + 3    /* 所在宮格x結束 */
			yS = y / 3 * 3 /* 所在宮格y起始 */
			yE = yS + 3    /* 所在宮格y結束 */
			for ; xS < xE; xS++ {
				for k = yS; k < yE; k++ {
					if t = SudokuData.data[xS][k].num; t == 0 {
						for ; t < SudokuData.data[xS][k].cnt; t++ {
							if tmpMay == SudokuData.data[xS][k].may[t] {
								goto NextFlagZ /* 這個可能值和在當前列不唯一 */
							}
						}
					}
				}
			}
			SudokuData.data[x][y].num = tmpMay /* 這個值在行上可能值中是唯一,填值並重新填值 */
			flagBreak = 1
			break
 
		NextFlagZ:
		}
	}
	if 1 == flagBreak {
		goto StartLoop
	}
 
	for i = 1; i < SudokuData.pos.cnt; i++ {
		x, y = SudokuData.pos.pos[i].i, SudokuData.pos.pos[i].j
		tmpMay = SudokuData.data[x][y].cnt
 
		for j = i - 1; j >= 0 && SudokuData.data[SudokuData.pos.pos[j].i][SudokuData.pos.pos[j].j].cnt > tmpMay; j-- {
			SudokuData.pos.pos[j+1].i = SudokuData.pos.pos[j].i
			SudokuData.pos.pos[j+1].j = SudokuData.pos.pos[j].j
		}
		SudokuData.pos.pos[j+1].i = x
		SudokuData.pos.pos[j+1].j = y
	}
 
	/* 下面列印可能點個數由少到多的排序 */
	//for i = 0; i < SudokuData.pos.cnt; i++ {
	//	fmt.Println(SudokuData.pos.pos[i], SudokuData.data[SudokuData.pos.pos[i].i][SudokuData.pos.pos[i].j])
	//}
	//fmt.Print("\n\n\n")
	//os.Exit(0)
	return true
}
 
/**
* 列印數獨
* 這裡需要win32api
* 將計算得到的資料上不同顏色
**/
func PrintSudoku() {
	var (
		i, j, tmp int
		api       = golibs.NewWin32Api()
	)
	fmt.Println(" ---------+---------+---------")
	for i = 0; i < Length; i++ {
		fmt.Print("|")
		for j = 0; j < Length; j++ {
			if tmp = SudokuData.data[i][j].num; tmp > 0 {
				if 0 == SudokuData.data[i][j].flag { /* 該位置是計算得到的,標紅色 */
					api.TextBackground(golibs.ForegroundRed | golibs.ForegroundIntensity)
				}
				fmt.Printf(" %d ", tmp) /* 下面把前景色重置為白色 */
				api.TextBackground(golibs.ForegroundRed | golibs.ForegroundGreen | golibs.ForegroundBlue)
			} else {
				fmt.Print(" . ")
			}
			if j == 2 || j == 5 {
				fmt.Print("|")
			}
		}
 
		switch i {
		case 2, 5:
			fmt.Print("|\n|---------+---------+---------|\n")
		case 0, 1, 3, 4, 6, 7:
			fmt.Println("|\n|         |         |         |")
		}
	}
	fmt.Println("|\n ---------+---------+---------")
}
 
/**
* 判斷當前成功沒
* 如果遊戲完成則返回true
* 否則沒有完成則返回false
**/
func GameComplete() bool {
	var i, j int
	for i = 0; i < Length; i++ {
		for j = 0; j < Length; j++ {
			if 0 == SudokuData.data[i][j].num {
				return false /* 數獨中存在沒有完成的位置,則遊戲還要繼續 */
			}
		}
	}
	return true /* 所有位置都完成 */
}

/**
* http://cn.sudokupuzzle.org/
* https://www.newdoku.com/zh/sudoku.php
* 上面是2個線上數獨網站
* 技巧:http://www.conceptispuzzles.com/zh/index.aspx?uri=puzzle/sudoku/techniques
* 規則:http://www.conceptispuzzles.com/zh/index.aspx?uri=puzzle/sudoku/rules
**/

執行結果

可通過執行Sudoku.exe -f Sudoku.txt來求解檔案中的數獨資料。下面就是一道數獨題,複製後儲存到Sudoku.txt中。

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

下面是結果,白色是題目數字,紅色部分是答案:

數獨結果

上面的方案效率在應對簡單級別的也是很快的,基本毫秒級別。但是比較蛋疼的就是暴力求解存在把所有解遍歷一遍的情況,那將遍歷非常大,雖然我已經保證每次把確定的值填入,但仍然無可避免窮舉的事實。測試過一個骨灰級的例子,用時44分鐘。好了上面就把我自己的想法寫成程式碼,並能正確得到結果,只是某些情況計算效率比較低,而且沒有處理存在多個值的情況。

舞蹈鏈求解數獨

求解數獨最佳方案當然是舞蹈鏈了,優點就是不會佔用多於空間,快取和恢復狀態非常快。
http://www.cnblogs.com/grenet/p/3145800.html 講解舞蹈鏈
http://www.cnblogs.com/grenet/p/3163550.html 講解如何用舞蹈鏈解數獨
程式碼靈感主要來源於上面的部落格,並且舞蹈鏈求解比較快,因此我也做了多解陣列至少算2種結果
    舞蹈鏈求解的具體流程就參照上面部落格吧,下面把我的程式碼貼上:

package main
 
import (
	"fmt"
	"log"
 
	"time"
 
	"io/ioutil"
 
	"flag"
 
	"github.com/jan-bar/golibs"
)
 
const (
	LenGrid    = 9                 /* 數獨都有9行9列格子 */
	Length     = LenGrid * LenGrid /* 數獨有81個元素 */
	NineDance  = 9 * Length        /* 81*9 建立出9個舞蹈鏈,分別代表填入的數字 */
	FourDance  = 4 * Length        /* 81*4 約束條件 */
	MinInitial = 1000000000        /* 最小min的初值 */
)
 
type Node struct {
	r, c  int /* 標識第r行,第c列 */
	up    *Node
	down  *Node
	left  *Node
	right *Node
}
 
var (
	SudokuData [Length + 1]int                /* 儲存數獨資料 */
	Mem1       [Length + 1]int                /* 儲存數獨結果1 */
	Mem2       [Length + 1]int                /* 儲存數獨結果2 */
	Mem        = &Mem1                        /* 用mem操作2個結果內的值 */
	Cnt        [FourDance + 1]int             /* 0-324  用於記錄0-324列,這一列有多少個結點 */
	Scnt       = 0                            /* 記錄數獨結果個數,本程式最多找到2個就退出 */
	Head       Node                           /* 頭結點 */
	All        [NineDance*FourDance + 99]Node /* 0-236294  構建729*324+99列的舞蹈鏈 */
	AllCnt     int                            /* 舞蹈鏈的遊標 */
	Row        [NineDance]Node                /* 0-728  構建729列的舞蹈鏈,用於1-9的填入,每個數字用81列來表示 */
	Col        [FourDance]Node                /* 0-323  構建324列的舞蹈鏈,用於滿足4個約束條件 */
)
 
func init() {
	fr := flag.String("f", "Sudoku.txt", "input data file!")
	flag.Parse()
 
	byt, err := ioutil.ReadFile(*fr)
	if err != nil {
		log.Fatal(err.Error())
	}
 
	var cnt = 0
	for _, v := range byt {
		if v >= '0' && v <= '9' {
			if cnt < Length { /* 數獨只有81個元素 */
				SudokuData[cnt] = int(v - '0')
			}
			cnt++
		}
	}
 
	if cnt != Length { /* 無論如何只有81個數字輸入 */
		log.Fatal("輸入檔案只能有81個數字!")
	}
	SudokuData[cnt] = MinInitial /* 標識結束符 */
	AllCnt = 1                   /* 舞蹈鏈從位置1開始 */
 
	Head.left = &Head  /* 頭結點的左邊是頭結點 */
	Head.right = &Head /* 頭結點的右邊是頭結點 */
	Head.up = &Head    /* 頭結點的上面是頭結點 */
	Head.down = &Head  /* 頭結點的下面是頭結點 */
	Head.r = NineDance /* 行數等於729 */
	Head.c = FourDance /* 列數等於324 */
 
	for cnt = 0; cnt < FourDance; cnt++ {
		Col[cnt].c = cnt          /* 324列舞蹈鏈 用0-323賦值給c */
		Col[cnt].r = NineDance    /* 把 729 賦給 r */
		Col[cnt].up = &Col[cnt]   /* 它的上面等於自己 */
		Col[cnt].down = &Col[cnt] /* 它的下面等於自己 */
 
		Col[cnt].left = &Head           /* 它的左邊等於頭結點 */
		Col[cnt].right = Head.right     /* 它的右邊等於頭結點的右邊 */
		Col[cnt].left.right = &Col[cnt] /* 它的左邊的右邊等於自己 */
		Col[cnt].right.left = &Col[cnt] /* 它的右邊的左邊等於自己 */
	}
 
	for cnt = 0; cnt < NineDance; cnt++ {
		Row[cnt].r = cnt       /* 729行舞蹈鏈,行數等於i */
		Row[cnt].c = FourDance /* 列數等於324 */
 
		Row[cnt].left = &Row[cnt]  /* 它的左邊等於自己 */
		Row[cnt].right = &Row[cnt] /* 它的右邊等於自己 */
 
		/* 頭結點下邊行的編號從上到下是728到0 */
		Row[cnt].up = &Head          /* 它的上邊等於頭結點 */
		Row[cnt].down = Head.down    /* 它的下邊等於頭結點的下邊 */
		Row[cnt].up.down = &Row[cnt] /* 它的上邊的下邊等於自己 */
		Row[cnt].down.up = &Row[cnt] /* 它的下邊的上邊等於自己 */
	}
 
	/* 訪問所有行,數獨舞蹈鏈中的第i行 表示 數獨中的第r行第c列中填入數字val */
	for cnt = 0; cnt < NineDance; cnt++ {
		var (
			r   = cnt / 9 / 9 % 9 /* 0-80  r為0   81-161 r為1 …… 648-728 r為8    表示數獨中的行    對映:舞蹈鏈行->數獨行 */
			c   = cnt / 9 % 9     /* 0-8  c為0   9-17 c為1   18-26  c為2   ……   72-80為8  迴圈直至720-728為8  81個為一週期   表示數獨中的列  對映:舞蹈鏈行->數獨列 */
			val = cnt%9 + 1       /* 0為1  1為2  2為3  ……  8為9   9個為一週期   表示數字1-9   對映:舞蹈鏈行->1-9數字 */
		)
		if SudokuData[r*9+c] == 0 || SudokuData[r*9+c] == val { /* r表示第r行,c表示第c列,如果數獨的第r行第c列是0-9 */
			/* 如果數獨的第r行第c列是0號則它的所有行都建立舞蹈鏈結點 */
			/* 如果數獨的第r行第c列是數字則它的指定行都建立舞蹈鏈結點 */
			Link(cnt, r*9+val-1)        /* 處理約束條件1:每個格子只能填一個數字    0-80列 */
			Link(cnt, Length+c*9+val-1) /* 處理約束條件2:每行1-9這9個數字只能填一個   81-161列 */
			tr := r / 3
			tc := c / 3
			Link(cnt, Length*2+(tr*3+tc)*9+val-1) /* 處理約束條件3:每列1-9的這9個數字都得填一遍 */
			Link(cnt, Length*3+r*9+c)             /* 處理約束條件4:每宮1-9的這9個數字都得填一遍 */
		}
	}
 
	/* 把728個行結點全部刪除 */
	for cnt = 0; cnt < NineDance; cnt++ {
		Row[cnt].left.right = Row[cnt].right /* 每一行左邊的右邊等於行數的右邊 */
		Row[cnt].right.left = Row[cnt].left  /* 每一行右邊的左邊等於行數的左邊 */
	}
}
 
/**
* 主程式入口
* http://aperiodic.net/phil/scala/s-99/
* https://www.newdoku.com/zh/sudoku.php
* http://www.cnblogs.com/grenet/p/3145800.html 講解舞蹈鏈
* http://www.cnblogs.com/grenet/p/3163550.html 講解如何用舞蹈鏈解數獨
**/
func main() {
	var tStart = time.Now() /* 開始時間 */
	Solve(1)
	var useTime = time.Since(tStart) /* 計算用時 */
 
	/* 下面列印數獨,初始化資料和列印都不計入運算時間 */
	switch Scnt {
	case 2:
		PrintSudoku(1)
		PrintSudoku(2)
		fmt.Print("  2個或者多個解的數獨")
	case 1:
		PrintSudoku(1)
		fmt.Print("  1個解的數獨")
	default:
		fmt.Print("  此數獨無解")
	}
	fmt.Println(",計算耗時:", useTime)
	fmt.Scanln() /* 避免一閃而逝 */
}
 
/**
* 用連結串列解釋就是一直插在第一個結點,以前的結點右推。
* 第r行,第c列
**/
func Link(r, c int) {
	Cnt[c]++          /* 第c列的結點增加了一個 */
	t := &All[AllCnt] /* 將指標指向下一個,就像線性表新增元素一樣 */
	AllCnt++
	t.r = r /* t的行數等於r */
	t.c = c /* t的列數等於c */
 
	t.left = &Row[r]       /* t的左邊等於第r行結點 */
	t.right = Row[r].right /* t的右邊等於第r行結點的右邊 */
	t.left.right = t       /* t的左邊的右邊等於t */
	t.right.left = t       /* t的右邊的左邊等於t */
 
	t.up = &Col[c]       /* t的上邊等於第c列結點 */
	t.down = Col[c].down /* t的下邊等於第c列下邊 */
	t.up.down = t        /* t的上邊的下邊等於t */
	t.down.up = t        /* t的下邊的上邊等於t */
}
 
/**
* 刪除這列的結點和結點所在行的結點
**/
func Remove(c int) {
	var t, tt *Node
	/* 刪除列結點 */
	Col[c].right.left = Col[c].left  /* 該列結點的右邊的左邊等於該列結點的左邊 */
	Col[c].left.right = Col[c].right /* 該列結點的左邊的右邊等於該列結點的右邊 */
 
	for t = Col[c].down; t != &Col[c]; t = t.down { /* 訪問該列的所有結點 直到回到列結點 */
		for tt = t.right; tt != t; tt = tt.right { /* 訪問該列所有結點所在的每一行 */
			Cnt[tt.c]-- /* 該列的結點減少一個 */
 
			/* 刪除該結點所在行中的一個結點 */
			tt.up.down = tt.down /* 該結點的上邊的下邊等於該結點的下邊 */
			tt.down.up = tt.up   /* 該結點的下邊的上邊等於該結點的上邊 */
		}
 
		/* 刪除該結點 */
		t.left.right = t.right /* t的左邊的右邊等於t的右邊 */
		t.right.left = t.left  /* t的右邊的左邊等於t的左邊 */
	}
}
 
/**
* 恢復一個節點
**/
func Resume(c int) {
	var t, tt *Node
	/* 遍歷該列結點 */
	for t = Col[c].down; t != &Col[c]; t = t.down {
		t.right.left = t /* 恢復t結點 */
		t.left.right = t /* 恢復t結點 */
 
		for tt = t.left; tt != t; tt = tt.left { /* 一直訪問左邊,直到回到t */
			Cnt[tt.c]++
			tt.down.up = tt
			tt.up.down = tt
		}
	}
	Col[c].left.right = &Col[c]
	Col[c].right.left = &Col[c]
}
 
/**
* 計算數獨
**/
func Solve(k int) {
	var (
		t, tt *Node
		min   = MinInitial
		tc    int
	)
 
	if Head.right == &Head { /* 得到一個數獨結果 */
		if Scnt == 0 { /* 首次得到結果 */
			for tc = 0; tc <= Length; tc++ {
				Mem2[tc] = Mem1[tc]
			}
			Mem = &Mem2 /* 將下一次計算的結果寫到Mem2中 */
		}
		Scnt++ /* 這裡第一種解決方案得到後,返回繼續 選行 來看有沒有第二種解決方案 */
		return
	}
 
	//fmt.Println(k) /* 列印每次查詢的行 */
	/* 從頭結點開始一直向右 直到回到頭結點
	   挑選結點數量最小的那一行,如果數量小於等於1直接用這行 */
	for t = Head.right; t != &Head; t = t.right {
		if Cnt[t.c] < min {
			min = Cnt[t.c]
			tc = t.c
			if min <= 1 {
				break
			}
		}
	}
	/* min==0的時候會把列刪除然後再把列恢復然後返回,說明之前選錯了行導致出現了結點為0的列,重新往下選擇一行。 */
	Remove(tc) /* 移除這一列 */
	/* 掃描這一列 直到 回到列結點 */
	for t = Col[tc].down; t != &Col[tc]; t = t.down {
		Mem[k] = t.r /* mem[k]儲存t的行數,最後可以通過行數來推斷數獨的幾行幾列填入了哪個數字 */
 
		/* 如果沒有這一步的話,在下面for迴圈的過程中會陷入死迴圈 */
		t.left.right = t /* 經檢查這兩個指標所指向的地址不同 */
 
		/* 開始訪問t的右邊 直到回到t。但是由於t在remove(tc)的過程中左右被跳過,所以tt!=t可能會一直成立,所以需要上一步來保證能回到t */
		for tt = t.right; tt != t; tt = tt.right {
			Remove(tt.c) /* 移除該行中所有帶結點的列 */
		}
 
		/* 等到該行的所有結點都刪除以後,把t結點徹底地刪除 */
		t.left.right = t.right
 
		Solve(k + 1)   /* 給下一個找行 */
		if Scnt >= 2 { /* 這裡找到2個解就退出 */
			return
		}
 
		/* 同上,避免死迴圈 */
		t.right.left = t
 
		/* 恢復所有被刪除的列 */
		for tt = t.left; tt != t; tt = tt.left {
			Resume(tt.c)
		}
 
		t.right.left = t.left /* 恢復t結點 */
	}
	Resume(tc) /* 恢復tc列,一旦跑出來了說明之前選錯了行,且如果一直回溯到一開始然後沒有更多的行可以選擇且scnt為0就說明沒有解決方案 */
}
 
/**
* 列印數獨
* 這裡需要win32api
* 將計算得到的資料上不同顏色
**/
func PrintSudoku(res int) {
	var (
		i, tmp int
		ans    [Length]int
		api    = golibs.NewWin32Api()
		mem    = &Mem1
	)
	if res == 2 { /* 確定列印那個結果 */
		mem = &Mem2
	}
 
	for i = 1; i <= Length; i++ {
		ans[mem[i]/9%Length] = mem[i]%9 + 1
	}
 
	fmt.Println(" ---------+---------+---------")
	for i = 1; i <= Length; i++ {
		if i%3 == 1 {
			fmt.Print("|")
		}
 
		if tmp = ans[i-1]; tmp > 0 {
			if SudokuData[i-1] == 0 { /* 該位置是計算得到的,標紅色 */
				api.TextBackground(golibs.ForegroundRed | golibs.ForegroundIntensity)
			}
			fmt.Printf(" %d ", tmp) /* 下面把前景色重置為白色 */
			api.TextBackground(golibs.ForegroundRed | golibs.ForegroundGreen | golibs.ForegroundBlue)
		} else {
			fmt.Print(" . ")
		}
 
		if i < Length {
			if i%27 == 0 {
				fmt.Println("|\n|---------+---------+---------|")
			} else if i%9 == 0 {
				fmt.Println("|\n|         |         |         |")
			}
		}
	}
	fmt.Println("|\n ---------+---------+---------")
}

用該方法求解【世界最難數獨】,速度也是嗖嗖的:

並且使用舞蹈鏈解法是可以解多個答案的數獨,不過有多解的數獨嚴格來講不能稱之為數獨。

總結

    演算法真是奇妙的東西,出了可以解決生活和工作中的各種問題,提高效率,還能破解遊戲。雖然玩數獨很有趣,破解數獨似乎對於我們這些程式設計師來說更刺激吧。

相關文章