暴力破解SixPack問題


  本文的內容是如何編寫代碼, 讓計算機來解決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()
    }
}

四. 總結

  運行程序, 問題完美解決. 結果如下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM