主頁 > 後端開發 > 求解數獨

求解數獨

2020-10-30 23:01:45 後端開發

目錄
  • 前言
  • 我的代碼
    • 代碼講解
    • 運行結果
  • 舞蹈鏈求解數獨
  • 總結

前言

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

我的代碼

代碼講解

????我想通過自己的思路來求解,雖然網上肯定有非常巧妙高效的解法,因此我安裝了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           = https://www.cnblogs.com/janbar/p/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 ---------+---------+---------")
}

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

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

總結

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/195905.html

標籤:Go

上一篇:【Flutter 混合開發】添加 Flutter 到 Android Fragment

下一篇:Go module 本地導包方式

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more