本文需要讀者有一定的魔方基礎, 最起碼也要達到十秒內還原二階魔方的水平, 並且手上最好有一個二階魔方, 否則文中的很多東西理解不了. 另外, 這里使用的算法是我自己寫着玩的, 如果你需要更成熟和專業的算法, 可以看這個. 本文最終得到的程序效果如下:
一. 問題分析
1. 魔方的數據結構
要使用程序計算魔方的解法, 第一步就需要設計一種數據結構來儲存當前的魔方狀態. 二階魔方有八個角塊, 我們可以把它們依次用0-7編號:
左后方看不到的角就是7號角塊. 然后, 每個角塊還有角度信息, 我們把白色和黃色作為基本色, 角塊的基本色朝上或者朝下, 角度記為0, 逆時針轉動120度后角度為0的記為1, 順時針轉動120度后角度為0的記為2. 這樣, 我們就可以通過兩個數字來儲存一個角塊信息.
一個魔方有八個角塊, 由於所有角塊的位置都是相對的, 因此我們可以假設7號角塊已經復原, 現在我們要用某個數據結構來表示剩余七個角塊的狀態. 為了提高運算速度並且方便查重, 我們可以使用一個二進制數字來表示當前的魔方狀態.
上圖這個長度為35的二進制數字, 可以分為七個部分, 從低位到高位這七個部分分別儲存了上圖中魔方七個角塊位置上的實際角塊信息. 每個角塊信息都是一個長度為5的二進制數, 其中前三位表示角塊的號碼, 后兩位表示角塊的角度. 這樣, 我們用一個數字就能表示魔方的狀態.
基於以上, 一個已經復原的魔方, 它的狀態就是下面的這串二進制數, 所有角塊的角度都為0, 角塊編號0-6從低到高排列:
2. 解法分析
本文算法的核心就是深度優先搜索, 通過遍歷所有可能的移動找出可行的解法. 二階魔方一共有URFLDB六個面可以轉動, 但是考慮到轉動的對稱性, 以及角塊位置的相對性, 在假設7號角塊已復原的前提下, 我們實際上只需要轉動URF這三個面.
二階魔方的最遠移動距離為11, 因此我們的遞歸深度到11就夠了. 考慮到每個面都有三種轉動角度(90度, 180度, 270度), 並且上一步操作的面不能和這一步相同, 因此在遞歸深度達到11的情況下, 可能的操作有3*3*(2*3)^10=544195584種, 而二階魔方的狀態數為7!*3^6=3674160種, 遠小於操作數. 因此我們還需要在遍歷過程中注意重復狀態.
基於以上, 本文算法核心部分的偽代碼如下:
解決函數: 參數:上一層操作的面,當前的遞歸深度,當前的魔方狀態 返回值:布爾值 # 首先處理邊界條件 如果當前的遞歸深度大於11: 返回 False 如果當前的魔方狀態在之前出現過,且那時的遞歸深度比現在淺: 返回 False 如果當前的魔方狀態是復原狀態: 記錄解法 返回 True 記錄當前的狀態 迭代URF三個面: 如果這個面不是上一層操作過的面: 迭代這個面的三種轉動角度: 調用解決函數, 傳入當前操作的面,當前的遞歸深度+1,轉動后的魔方狀態 如果解決了: 返回 True 返回 False
3. 還原7號角塊
上面1, 2節的分析都是基於一個前提, 即7號角塊已經復原了, 但是這個前提未必是成立的. 因此, 我們需要在運算之前加上一個步驟, 這個步驟不轉動魔方的任何一面, 而是通過x,y,z方向的轉體將7號角塊還原到正確的位置上, 同時記錄下這些xyz的操作. 在計算出魔方的解法之后, 再基於之前的xyz操作對解法進行修正.
4. 程序的結構和流程
基於以上分析, 我們程序的結構大致如下:
首先, 我們創建一個接口以方便外界的調用. 在獲取到用戶給出的數據之后, 首先通過解析層把用戶給出的數據解析為上述的數據結構, 並且預處理還原7號角塊, 然后將解析后的信息交給運算部分計算結果, 最后將結果基於之前的預處理進行修正, 將修正結果返回給用戶.
二. 算法的核心部分
基於上一章的分析, 這個算法的核心部分應該接收一個以長度為35的二進制數字所表示的魔方狀態, 然后通過URF的移動將這個狀態轉變為復原態, 並最終返回達到復原態所經歷的移動路徑.
1. cube結構體
魔方的求解過程會產生很多變量, 因此我們首先定義一個結構體來儲存這些變量, 它們的作用會在后續體現:
type cube struct { prefix []string state uint64 moves [11]string moveLength uint8 seen map[uint64]uint8 solved bool ans string }
2. 轉動魔方的面
魔方轉動的本質就是改變旋轉面上四個角塊的位置和方向, 從而使魔方從某個狀態移動到另一個狀態. 由於我們把所有的角塊信息都放在一個狀態數字里面了, 因此在數據的層面, 轉動這個魔方分為三個步驟: 首先, 將對應面的四個角塊信息從狀態數字中提取出來; 然后, 修改提取出來的四個角塊的位置和方向信息; 最后, 將修改后的角塊填回狀態數字, 覆蓋原有的信息. 這樣我們就得到了轉動后的狀態.
基於以上的分析, 我們需要定義如下幾個函數:
var layerDigits = map[string][4]int{ "U": [...]int{0, 1, 2, 3}, "R": [...]int{2, 5, 4, 3}, "F": [...]int{1, 6, 5, 2}, } // 這個函數從某個狀態中獲取指定層的角塊信息 func extractCorners(state uint64, layer string) []uint8 { corners := make([]uint8, 4) for i, digit := range layerDigits[layer] { corners[i] = uint8((state & (0b11111 << (digit * 5))) >> (digit * 5)) } return corners } // 這個函數給定四個角塊和它們所在的層,返回順時針轉動九十度后的四個角塊 func turnCorners(corners []uint8, layer string) []uint8 { corners = append(corners[1:], corners[0]) if layer == "R" || layer == "F" { corners[1], corners[3] = turnCorner(corners[1], "f"), turnCorner(corners[3], "f") corners[0], corners[2] = turnCorner(corners[0], "r"), turnCorner(corners[2], "r") } return corners } // 這個函數將某個角塊順時針或逆時針轉動120度,返回轉動后的角塊 // method為f則順時針,為r則逆時針 func turnCorner(corner uint8, method string) uint8 { angle := corner & 0b11 if method == "f" { angle++ if angle == 3 { angle = 0 } } else if method == "r" { if angle == 0 { angle = 3 } angle-- } corner &= 0b11100 return corner | angle } // 這個函數把給定的角塊信息填入某個狀態的指定位置,返回修改后的狀態 func fillCorners(state uint64, corners []uint8, layer string) uint64 { for i, digit := range layerDigits[layer] { state &= ^(0b11111 << (digit * 5)) state |= (uint64(corners[i]) << (digit * 5)) } return state }
這幾個函數使用到了位運算, 如果對位運算還不夠了解, 推薦看這篇文章.
通過上面幾個函數, 我們就可以旋轉魔方並得到旋轉后的狀態了. 首先, 通過extractCorners函數提取到指定的URF的某個面四個角塊的信息數組, 每塊的信息用一個長度為5的二進制數表示; 然后, 通過turnCorners函數將得到的數組中的元素位置進行交換, 這樣就等同於交換了指定面四個角塊的位置, 此外如果是R操作或者F操作, 角塊的方向也會變化, 因此再創建一個turnCorner函數來單獨修改單個角塊的方向信息; 上述步驟完成后, 我們就得到了轉動后的四個角塊, 調用fillCorners將轉動后的角塊填入原state中, 得到的新state就是魔方轉動后的狀態.
3. 深度優先搜索
在實現了對魔方的轉動后, 我們就可以開始搜索解法了. 遍歷所有可能的操作, 直到經過這些操作后的魔方狀態為還原態為止, 這樣我們就得到了魔方的解法:
const restoredState = 0b11000101001000001100010000010000000 var layers = [...]string{"U", "R", "F"} var layerAngles = [...]int{1, 2, 3} func (c *cube) solve(lastLayer string, index uint8) bool { if index >= 11 { return false } else if moveLength, exists := c.seen[c.state]; exists && moveLength < index { return false } else if c.state == restoredState { c.moveLength = index c.solved = true return true } c.seen[c.state] = index state := c.state for _, layer := range layers { if layer == lastLayer { continue } corners := extractCorners(c.state, layer) for _, angle := range layerAngles { corners = turnCorners(corners, layer) c.state = fillCorners(state, corners, layer) c.moves[index] = fmt.Sprintf("%s%d", layer, angle) if c.solve(layer, index+1) { return true } } c.state = state } c.moves[index] = "" return false }
4. 代碼測試
通過上面的一百行代碼, 我們就完成了整個解魔方算法的核心部分. 對它簡單地進行一點測試, 結果如下:
func test() { cb := &cube{ state: 0b01000000100110110100100101101000110, seen: make(map[uint64]uint8), } cb.solve("", 0) fmt.Println(cb.moves[:cb.moveLength]) }
輸出結果如下:
經過多種情況的測試, 可以確定, 我們這個算法是可行的, 能夠解出任意狀態正確的二階魔方.
三. 解析層
解析層需要做三件事: 首先, 解析用戶給的魔方狀態, 把數據轉化為我們定義的結構類型; 然后, 完成7號角塊復原的工作, 此時數據已經可以交給運算層去計算了; 最后, 把計算結果轉化為用戶需要的格式返回.
1. 用戶層面的魔方數據結構
對於用戶來說, 使用二進制數據來表示魔方狀態顯然是不方便也不直觀的, 因此我們使用顏色來表示魔方的狀態. 二階魔方一共有2x2x6=24片, 我們用0-23給每片的位置進行編號:
按照魔方復原之后白綠紅藍橙黃的順序, 為這六個面的每個位置編號如上. 然后, 我們再使用WGRBOY六個字母代表六種顏色, 這樣我們就能用一個長度為24的字符串來表示魔方的狀態了, 字符串中第i位代表的就是上圖中標號為i位置的顏色, 比如對於一個還原的魔方來說, 它的字符串為WWWWGGGGRRRRBBBBOOOOYYYY.
2. 用戶數據的解析和驗證
首先, 我們定義一個函數, 把用戶給出的數據結構轉變為二進制數字的數據結構:
var patternIndexes = [8][3]int{ {0, 16, 13}, {2, 4, 17}, {3, 8, 5}, {1, 12, 9}, {23, 11, 14}, {21, 7, 10}, {20, 19, 6}, {22, 15, 18}, } var cornerID = map[string]int{ "WOB": 0, "WGO": 1, "WRG": 2, "WBR": 3, "YRB": 4, "YGR": 5, "YOG": 6, "YBO": 7, } func patternToCorners(pattern string) []uint8 { if len(pattern) != 24 { panic(fmt.Sprintf("Invalid cube pattern")) } corners := make([]uint8, 8) for i := 0; i < 8; i++ { cornerPattern := make([]string, 3) var id, angle int for j, index := range patternIndexes[i] { cornerPattern[j] = string(pattern[index]) if cornerPattern[j] == "W" || cornerPattern[j] == "Y" { angle = j } } cornerPattern = append(cornerPattern[angle:], cornerPattern[:angle]...) if _, ok := cornerID[strings.Join(cornerPattern, "")]; ok { id = cornerID[strings.Join(cornerPattern, "")] } else { panic(fmt.Sprintf("Invalid corner:%v", cornerPattern)) } corners[i] = uint8((id << 2) | angle) } return corners }
每個角塊都有編號和朝向兩個屬性, 我們首先從pattern中取出三片色塊, 將它們組裝為一個角塊, 同時計算出這個角塊的角度, 通過字典查找出角塊的編號, 最后將角塊編號和角塊朝向結合起來就行了.
在解析完用戶數據后, 我們還需要驗證用戶給的信息是否合法, 以避免無謂的計算. 一個二階魔方狀態合法的條件是: 0-7號角塊都存在且唯一, 並且這些角塊的朝向和為3的倍數. 因此, 我們使用如下的一個函數就能驗證數據合法性:
func isCubeValid(corners []uint8) bool { existCorners := make([]bool, 8) var angleSum uint8 for _, corner := range corners { id, angle := corner>>2, corner&0b11 existCorners[id] = true angleSum += angle } for _, exist := range existCorners { if !exist { return false } } return angleSum%3 == 0 }
3. 還原7號角塊
在第一章我們講了, 可以通過x,y,z方向的整體移動來復原7號角塊的位置和朝向. 因此, 我們首先就要定義三個函數, 這三個函數能夠對剛解析得到的數據進行修改, 得到x,y,z移動之后的魔方狀態:
func xMove(corners []uint8, times int) []uint8 { newCorners := make([]uint8, 8) for m, n := range []int{7, 0, 3, 4, 5, 2, 1, 6} { newCorners[m] = corners[n] } corners = newCorners for i, corner := range corners { if i%2 == 0 { corners[i] = turnCorner(corner, "r") } else { corners[i] = turnCorner(corner, "f") } } if times == 1 { return corners } return xMove(corners, times-1) } func yMove(corners []uint8, times int) []uint8 { corners = append(corners[1:4], corners[0], corners[7], corners[4], corners[5], corners[6]) if times == 1 { return corners } return yMove(corners, times-1) } func zMove(corners []uint8, times int) []uint8 { newCorners := make([]uint8, 8) for m, n := range []int{7, 6, 1, 0, 3, 2, 5, 4} { newCorners[m] = corners[n] } corners = newCorners for i, corner := range corners { if i%2 == 0 { corners[i] = turnCorner(corner, "f") } else { corners[i] = turnCorner(corner, "r") } } if times == 1 { return corners } return zMove(corners, times-1) }
這三個函數分別將魔方整體向x,y,z方向順時針移動90度, 與上一章出現的turnCorners函數比較類似, 都是移動數組元素的位置, 有時候還需要修改角度. 只不過這里的數組長度變為8. 為了代碼的簡潔, 需要多次移動的情況使用遞歸調用來實現.
完成這三個函數之后, 我們就可以考慮如何復原7號角塊了, 這里可以分為六種情況:
如果7號角塊的角度為0:
如果7號角塊在上層:
通過yMove移動到3號位置,然后通過兩次zMove歸位
如果7號角塊在下層:
通過yMove歸位
如果7號角塊的角度為1:
如果7號角塊在上層:
通過yMove移動到0號位置, 然后通過三次zMove歸位
如果7號角塊在下層:
通過yMove移動到4號位置, 然后通過一次zMove歸位
如果7號角塊的角度為2:
如果7號角塊在上層:
通過yMove移動到0號位置, 然后通過三次xMove歸位
如果7號角塊在下層:
通過yMove移動到6號位置, 然后通過一次zMove歸位
因為規律不明顯, 所以這里只能硬編碼了, 沒有非常好的辦法:
func getPrefix(corners []uint8) (newCorners []uint8, prefix []string) { var i int // 首先找出7號角塊的位置 for j, c := range corners { if c>>2 == 0b111 { i = j break } } corner := corners[i] angle := corner & 0b11 if angle == 0 { if i > 3 { for j := 0; j < 7-i; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = corners[:7] return } for j := 0; j < (i+1)%4; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = zMove(corners, 2)[:7] prefix = append(prefix, []string{"Z", "Z"}...) return } else if angle == 1 { if i > 3 { for j := 0; j < (8-i)%4; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = zMove(corners, 1)[:7] prefix = append(prefix, "Z") return } for j := 0; j < i; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = zMove(corners, 3)[:7] prefix = append(prefix, []string{"Z", "Z", "Z"}...) return } else if angle == 2 { if i > 3 { yMoves := []int{2, 1, 0, 3} for j := 0; j < yMoves[i-4]; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = xMove(corners, 1)[:7] prefix = append(prefix, "X") return } for j := 0; j < i; j++ { corners = yMove(corners, 1) prefix = append(prefix, "Y") } newCorners = xMove(corners, 3)[:7] prefix = append(prefix, []string{"X", "X", "X"}...) return } else { panic("It's impossible,haha") } }
在調用這個函數之后, 我們就復原了7號角塊, 並且得到前置操作(x,y,z)和剩余0-6號角塊組成的數組, 最后, 我們將這個數組轉化為一個長度為35的二進制數字, 解析層的前期工作就算完成了:
var state uint64 for i, corner := range corners { state |= uint64(corner) << (i * 5) }
c.state = state
c.prefix = prefix
4. 格式化運算結果
在程序的運算層計算出結果之后, 我們還需要將運算結果與上一節得到的前置操作整合, 並最終格式化為用戶需要的結果:
func mergeResult(prefix []string, moves []string) string { // 這個字典記錄了x,y,z操作對層轉動的影響, key為影響前,value為影響后 dic := map[string]map[string]string{ "X": { "U": "B", "R": "R", "F": "U", "L": "L", "B": "D", "D": "F", }, "Y": { "U": "U", "R": "B", "F": "R", "L": "F", "B": "L", "D": "D", }, "Z": { "U": "L", "R": "U", "F": "F", "L": "D", "B": "B", "D": "R", }, } for i := len(prefix) - 1; i >= 0; i-- { for j, move := range moves { moves[j] = dic[prefix[i]][move[:1]] + move[1:] } } return strings.ReplaceAll(strings.ReplaceAll(strings.Join(moves, " "), "3", "'"), "1", "") }
5. 步驟整合和異常處理
上述的步驟比較多和混亂, 並且有些步驟可能會拋出異常. 因此我們將這些步驟進行整合:
// New 函數解析和驗證給定的圖案,如果不合法,返回err,否則把解析結果創建cube對象返回 func New(pattern string) (c *cube, err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("%v", r) } } }() state, prefix := parsePattern(pattern) c = &cube{ state: state, prefix: prefix, seen: make(map[uint64]uint8), } return } func parsePattern(pattern string) (state uint64, prefix []string) { corners := patternToCorners(pattern) if !isCubeValid(corners) { panic("Invalid cube pattern") } corners, prefix = getPrefix(corners) for i, corner := range corners { state |= uint64(corner) << (i * 5) } return }
這樣我們就把解析層放在了實例化cube結構體之前的位置, 進行一些簡單的測試, 結果如下:
func test() { c, _ := cube.New("WOWGGYGGRWWRBRBBBOOOYRYY") fmt.Println(c.Solve()) }
從一個包的角度來講, 我們提供了一個簡單易用的接口. 不過, 根本原因是目前提供的功能不多.
四. HTTP接口
為了讓外界更方便地調用這個程序, 我們創建一個http服務器來提供接口:
import ( "encoding/json" "fmt" "log" "net/http" "time" "./cube" ) func cubeServer(w http.ResponseWriter, r *http.Request) { args, ok := r.URL.Query()["pattern"] if !ok || len(args) < 1 { return } res := map[string]interface{}{ "solved": true, "ans": "", "msg": "", } c, err := cube.New(args[0]) if err != nil { res["solved"] = false res["msg"] = err.Error() } else { start := time.Now().UnixNano() res["ans"] = c.Solve() fmt.Printf("用時%.3f秒\n", float64(time.Now().UnixNano()-start)/float64(1e9)) } bytes, _ := json.Marshal(res) w.Write(bytes) } func main() { http.HandleFunc("/", cubeServer) err := http.ListenAndServe("localhost:8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }
通過瀏覽器訪問這個接口, 結果如下:
五. 客戶端
為了讓用戶更加方便和直觀地輸入魔方狀態, 我們可以創建一個客戶端. 解決方案大致有前端, GUI和openCV三種, 這里使用python的tkinter模塊完成一個GUI客戶端:
from tkinter import * import json import requests URL = '127.0.0.1:8080' UNIT = 120 # 魔方一個塊的大小,整個界面的布局基本由它決定 COLORS = ('white', 'green', 'red', 'blue', 'orange', 'yellow') FONT = ('charter', 14) class GUI: def __init__(self): self.root = Tk() self.root.title('魔方求解器') self.canvas = Canvas(self.root, width=UNIT * 8 + 10, height=6 * UNIT + 10) self.canvas.pack() # 各種組件往上加就完事了 self.url = self._add_url_text() self.info_bar = self._add_info_bar() self.current_color = COLORS[0] self.view = self._add_view() self.picks = self._add_color_picker() self._add_buttons() self.canvas.bind('<Button-1>', self._click) self.root.mainloop() def _add_url_text(self) -> Text: self.canvas.create_window(10, 10 + 0.5 * UNIT, anchor=NW, window=Label(text='服務器地址:', font=FONT)) txt_url = Text(height=1, width=20, font=FONT) txt_url.insert(INSERT, URL) self.canvas.create_window(10, 10 + 0.8 * UNIT, anchor=NW, window=txt_url) return txt_url def _add_info_bar(self) -> Text: self.canvas.create_window(10 + 4.1 * UNIT, 10 + 0.45 * UNIT, anchor=NW, window=Label(text='信息欄:', font=FONT)) info_bar = Text(height=4, width=40, font=FONT) self.canvas.create_window(10 + 4.1 * UNIT, 10 + 0.75 * UNIT, anchor=NW, window=info_bar) return info_bar def _add_view(self) -> [[[int]]]: # 創建展開圖,返回展開圖中所有塊的id coordinate = ((1, 0), (1, 1), (2, 1), (3, 1), (0, 1), (1, 2)) view = [[[0] * 2 for _ in range(2)] for _ in range(6)] for f in range(6): for r in range(2): y = 10 + coordinate[f][1] * 2 * UNIT + r * UNIT for c in range(2): x = 10 + coordinate[f][0] * 2 * UNIT + c * UNIT view[f][r][c] = self.canvas.create_rectangle(x, y, x + UNIT * 0.95, y + UNIT * 0.95, fill=COLORS[f]) return view def _add_color_picker(self) -> [int]: # 創建調色板,返回調色板所有塊的id picks = [0 for _ in range(6)] width = UNIT * 0.6 for i in range(6): x = (i % 3) * (width + 5) + UNIT * 4.5 y = (i // 3) * (width + 5) + UNIT * 4.5 picks[i] = self.canvas.create_rectangle(x, y, x + width, y + width, fill=COLORS[i]) self.canvas.itemconfig(picks[0], width=4) return picks def _add_buttons(self) -> None: self.canvas.create_window(10 + 6.6 * UNIT, 10 + 4.6 * UNIT, anchor=NW, window=Button(text='求解', height=1, width=10, relief=RAISED, command=self._solve, font=FONT)) self.canvas.create_window(10 + 6.6 * UNIT, 10 + 5.1 * UNIT, anchor=NW, window=Button(text='重置', height=1, width=10, relief=RAISED, command=self._reset, font=FONT)) def _solve(self) -> None: url = self.url.get(1.0, END) if not url.startswith('http'): url = f'http://{url}' try: r = requests.get(url, params={ 'pattern': ''.join( self.canvas.itemcget(char, 'fill')[0] for face in self.view for row in face for char in row).upper() }) assert r.status_code == 200 except: self._show_info('連接服務器失敗,檢查你的url和網絡') return else: res = json.loads(r.text) if not res['solved']: self._show_info(f'求解過程中出現了問題: {res["msg"]}') else: self._show_info(f'這個魔方的解法是: {res["ans"]}') def _reset(self) -> None: for f, face in enumerate(self.view): for row in face: for i in row: self.canvas.itemconfig(i, fill=COLORS[f]) self._show_info('') def _click(self, _) -> None: # 響應canvas點擊事件 click_id = self.canvas.find_withtag('current') if not click_id: return if click_id[0] in self.picks: # 如果點在顏色選擇器上,就修改當前選中的顏色 self.current_color = self.canvas.itemcget('current', 'fill') for i in range(6): self.canvas.itemconfig(self.picks[i], width=1) self.canvas.itemconfig('current', width=5) else: # 否則就是點在展開圖上,修改展開圖對應塊的顏色 self.canvas.itemconfig('current', fill=self.current_color) def _show_info(self, info: str) -> None: self.info_bar.delete(0.0, END) self.info_bar.insert(INSERT, info) if __name__ == '__main__': GUI()
這東西沒有什么好講的, 原理就是在畫布上添加一些正方形, 然后通過點擊事件給這些正方形上色. 最終成果如下:
六. 總結
經過一些測試之后, 發現部分狀態的耗時時間明顯比其它狀態長很多:
魔方的移動路徑實際上是一顆樹, 其高度只有11層, 而葉子節點的數量達到了3*3*(2*3)^10=544195584個, 因此, 這棵樹是非常扁平的.
假設魔方的某個狀態, 其還原步驟都在g的子樹上, 我們使用深度優先搜索, 需要依次遍歷b,c,d,e,f子樹以及它們的所有子節點, 找不到之后才會去g尋找, 這樣就會非常吃虧. 從這點來看, 使用廣度優先搜索或許是更好的選擇. 這部分就先挖個坑, 等哪天想起來了, 再研究研究.