本文的內容是如何編寫代碼, 讓計算機來解決SixPack拼圖問題. 使用語言為Golang, 沒有什么干貨.
一. 問題分析
最近從朋友那里拿到了一副拼圖, 據說難度高達九級, 它大概長這樣:
這幅拼圖的目標是把八片拼圖依次填滿六個不同的空白位置. 八片拼圖的每片都是由八個1x1大小的方塊結合而成, 而給出的六個puzzle則是由64個1x1大小的空位組成. 因此, 這個問題用計算機的方式來描述就是: 如何將八個給定的二維數組結合成為指定形狀.
由於狀態數不多, 因此這里使用暴力破解的方式解決問題. 首先, 每片拼圖都可以旋轉和翻轉, 因此我們需要計算出每片拼圖的八個狀態; 然后, 我們創建一個空白部分和puzzle一致的二維數組, 依次取出這八片拼圖, 嘗試每片拼圖每種狀態下所有可能放置的位置, 直到把所有拼圖都放進去為止.
綜上所述, 解決這個問題的偽代碼如下:
解決函數: 如果八片拼圖都用完了: 返回結果 # 邊界條件 從剩余拼圖取出一片 迭代這片拼圖所有可能的狀態: 迭代拼圖在這個狀態下所有可能放入的位置: 放入拼圖 繼續調用自己 如果得出結果, 就跳出循環 放回拼圖
二. Piece部分代碼
首先, 我們使用一個結構體來表示每片拼圖:
type piece struct { basicShape [][]string canRotateTwice bool canFlip bool states [][][]string } func (p *piece) GetStates() [][][]string { if len(p.states) < 1 { p.states = append(p.states, getDirections(p.basicShape, p.canRotateTwice)...) if p.canFlip { flippedShape := flipState(p.basicShape) p.states = append(p.states, getDirections(flippedShape, p.canRotateTwice)...) } } return p.states } // 調用這個函數獲取某個狀態不翻轉情況下的所有朝向 // 如果拼圖是中心對稱的,得到兩種朝向狀態,否則得到四種 func getDirections(state [][]string, canRotateTwice bool) [][][]string { n := 2 if canRotateTwice { n *= 2 } result := make([][][]string, n) for i := 0; i < n; i++ { result[i] = state state = rotateState(state) } return result } // 返回拼圖的某個狀態順時針轉動九十度后的狀態,不會修改原數據 func rotateState(state [][]string) [][]string { m, n := len(state[0]), len(state) result := make([][]string, m) for i := range result { result[i] = make([]string, n) } for i := 0; i < m; i++ { for j := 0; j < n; j++ { result[i][n-j-1] = state[j][i] } } return result } // 返回拼圖的某個狀態左右翻轉后的狀態,不會修改原數據 func flipState(state [][]string) [][]string { result := make([][]string, len(state)) for i, list := range state { row := make([]string, len(list)) for j := 0; j < len(list); j++ { row[j] = list[len(list)-j-1] } result[i] = row } return result }
在實例化一片拼圖時, 我們需要傳入一個原始狀態, 程序會通過旋轉和翻轉原始狀態計算出這片拼圖的所有八個狀態, 然后調用GetStates方法就能獲取這片拼圖的所有狀態. 由於有的拼圖是左右對稱或者中心對稱的, 八個狀態有重復, 因此我們還需要設置結構體的canRotateTwice和canFlip屬性, 以減少不必要的重復狀態.
最后, 這個piece結構體就可以通過下面的形式使用:
a := "A" p := &piece{ basicShape: [][]string{{a, a, a, a}, {a, a, a, a}}, canRotateTwice: false, canFlip: false, } states := p.GetStates()
三. 遞歸部分代碼
在計算出拼圖的狀態后, 下一個步驟就是把拼圖填入到puzzle的空白部分中, 使用如下代碼實現:
func tryToPut(state [][]string, puzzle [][]string, i int, j int) (bool, [][]string) { puzzle = copyPuzzle(puzzle) for a := 0; a < len(state[0]); a++ { for b := 0; b < len(state); b++ { if state[b][a] != empty { if puzzle[j+b][i+a] != empty { return false, puzzle } puzzle[j+b][i+a] = state[b][a] } } } return true, puzzle } func copyPuzzle(puzzle [][]string) [][]string { result := make([][]string, len(puzzle)) for i, list := range puzzle { row := make([]string, len(list)) copy(row, list) result[i] = row } return result }
由於拼圖和puzzle都不是規則形狀的, 因此我們在二維數組中設置一個empty變量來表示空白的部分. 一片拼圖能夠放入puzzle指定位置的條件是: 這片拼圖的二維數組與puzzle二維數組相重疊的位置, 要么拼圖是empty, 要么puzzle是empty.
能夠放入拼圖后, 剩下的就簡單了. 根據第一節的偽代碼寫好一個遞歸函數就完事:
func solve(puzzle [][]string, i int) (bool, [][]string) { if i == len(piece.Pieces) { return true, puzzle } for _, state := range piece.Pieces[i].GetStates() { for _, nextPuzzle := range putIntoPuzzle(state, puzzle) { solved, result := solve(nextPuzzle, i+1) if solved { return solved, result } } } return false, [][]string{} } // 這個函數將一片拼圖放入puzzle中,返回所有可以放置的結果,不會對原puzzle切片產生影響 func putIntoPuzzle(state [][]string, puzzle [][]string) [][][]string { result := [][][]string{} for i := 0; i < len(puzzle[0])-len(state[0])+1; i++ { for j := 0; j < len(puzzle)-len(state)+1; j++ { ok, newPuzzle := tryToPut(state, puzzle, i, j) if ok { result = append(result, newPuzzle) } } } return result }
最后, 我們運行solve函數, 並且把結果寫入文本文件中:
func main() { var startTime int64 outputFile, outputError := os.OpenFile("res.txt", os.O_WRONLY|os.O_CREATE, 0666) if outputError != nil { panic("無法打開res.txt文件!!!") } defer outputFile.Close() outputWriter := bufio.NewWriter(outputFile) for i, puzzle := range puzzles { startTime = time.Now().UnixNano() solved, result := solve(puzzle, 0) if solved { fmt.Fprintf(outputWriter, "puzzle%d和對應解法,用時%.2f秒\n\n", i+1, float64(time.Now().UnixNano()-startTime)/float64(1e9)) writePuzzle(outputWriter, merge(puzzle, result), true) } else { fmt.Fprintf(outputWriter, "沒能解開puzzle%d,用時%.2f秒\n\n", i+1, float64(time.Now().UnixNano()-startTime)/float64(1e9)) writePuzzle(outputWriter, puzzle, false) } outputWriter.Flush() } }
四. 總結
運行程序, 問題完美解決. 結果如下: