拼圖問題又叫N數碼問題。這個問題比較簡單,基本上有一個人研究透徹之后就再也沒有研究價值了。
2010年《計算機應用軟件》上發表的一篇論文《N數碼問題直接解與優化問題研究》對N數碼問題的可解性和直接解法進行了透徹的研究。
此[repo](https://github.com/weiyinfu/pintu)提供了一個拼圖自動求解算法(非最優解)。
一.拼圖問題定義
給定一個m行n列的平面方格圖(m!=1&&n!=1),只有一個空位,其余每個方格內為1~(m*n-1)的數字.可以將空格與其上下左右相鄰方格內的卡片交換位置.目標就是從左到右,從上到下依次排成從1到(m*n-1)的陣列,空位在最后一格內.
二.定義:拼圖某狀態的逆序數
從左到右,從上到下,各個格點內的數字形成一個序列,這個序列的逆序數就是當前狀態的逆序數.對於任意一個拼圖,目標狀態的逆序數一定是0,因為肯定是1,2,3....這樣排列的.
三.操作對拼圖逆序數的影響
對於一個狀態,可以將空格與其上下左右4個位置的卡片交換位置.左右交換不影響狀態的逆序數,這是顯然易見的.
上下交換,相當於多次交換.當列數為奇數,上下交換相當於交換偶數次,奇偶性不變;當列數為偶數,上下交換相當於交換奇數次,奇偶性變化.
例如,狀態[1,2,3;4,_,6;5,7,8]的逆序列為12346578.將空格與空格下方的7交換位置,變成12347658,相當於先是7與5換,然后再跟6換,交換了偶數次,逆序數不變.
所以,操作是否影響奇偶性取決於列數的奇偶性.
四.空格狀態的奇偶性
如果空格所在行與目標行的行距為偶數,則稱空格狀態為偶數性;若為奇數,則稱空格狀態為奇數性.
五.拼圖問題可解的充要條件
知道目標狀態,知道操作過程,就足以攻克一切問題.
操作與奇偶性的關系有兩種:左右交換始終不影響奇偶性.(1)列數為奇數,上下交換不影響奇偶性;(2)列數為偶數,上下交換影響奇偶性.
關鍵在於找到操作中的守恆量,雖然每一個操作都會產生下一個狀態,但是這個過程中有守恆量:
如果列數為奇數,狀態逆序數的奇偶性守恆.
如果列數為偶數,狀態逆序數的奇偶性^空位狀態的奇偶性守恆.其中^表示異或運算.
於是結論是,當列數為奇數時,一切操作不影響奇偶性,當前狀態逆序數為偶數 等價於 拼圖有解.
當列數為偶數時,上下交換影響奇偶性,只要當前狀態逆序數奇偶性^當前空格狀態的奇偶性=偶數 等價於 拼圖有解.其中^符號表示異或運算.
一言以蔽之,拼圖有解定理就是:當前狀態守恆量的值為偶數.
六.證明:拼圖有解=>當前狀態守恆量的值為偶數
對於列數為奇數的拼圖,操作中滿足狀態逆序數奇偶性不變,所以只有當前狀態與目標狀態奇偶性一致才有可能有解.
對於列數為偶數的拼圖,操作中滿足狀態逆序數奇偶性^當前空格狀態奇偶性不變,所以只有當前狀態的逆序數奇偶性^當前空格狀態奇偶性與目標狀態一致才有可能有解.
這個問題蘊含的道理十分豐富:
(1)分析變化的事物要找到變化中的守恆量.
(2)要注重開頭和結尾,不要在意中間的過程.
七.證明:拼圖守恆量的值與目標狀態相同=>拼圖有解
把拼圖分成四個部分:左上角的m-2行n-2列、下面的2行n-2列、右面的m-2行2列、右下角的2行2列,這四部分分別記作A、B、C、D。完成順序為A、B、C、D,逐塊拼成。
A部分很容易拼成,不必贅言。
B、C兩部分同構,只需要討論其中一個。
D部分不用說了,2行2列太簡單了。
下面重點討論B部分。
第一步,先處理好1位置;第二步,把1上面的鄰居挪到4位置;第三步,把空格挪到5。這三步都是輕而易舉可以完成的。
至此就可以應用一個固定的“公式”。讓1迎接4位置回家。
上述證明的思想就是,構造幾個操作,某些區塊它們能夠不影響別人,而把自己調整成正確的狀態.
上面是以行少列多為例,對於行多列少的情況顯然也成立.
八.關於拼圖問題的其他結論
(1)將空格移動到右下角后拼圖狀態逆序數奇偶性為偶數<=>拼圖有解.
(2)交換任意兩個非空格塊(可以不相鄰),有解的會變成無解,無解的會變成有解.
(3)將空格移動到右下角后,若有偶數對方塊正好顛倒,問題有解;若有奇數對方塊顛倒,問題無解.
(4)拼圖的狀態構成一張圖,邊就是操作.拼圖的結點有兩種(有解和無解),有解的結點必然能夠到達目標結點,目標結點也能到達它們,所以有解結點集是連通的,無解結點集其實也是連通的,此圖有兩個連通分量.但不知道如何證明.
九.應用
生成拼圖問題時,關鍵是要保證拼圖有解.一種方法是先生成目標狀態,一番隨機操作打亂之.這種方法在拼圖行數列數較小時比較適用,一旦拼圖規模變大,隨機操作的次數不夠就容易生成很簡單的拼圖.
另一種方法就是利用拼圖有解的充要條件.隨機生成拼圖序列,如3*3的拼圖隨機生成為312450678,其中0表示空位.然后判斷它是否有解,如果無解交換兩個非空方格內的數字,如果有解,就更好了.這種方法對拼圖的打亂強度比較大,很容易生成雜亂無章的拼圖.
十.以2行4列拼圖為例檢驗一下結論
//一個2行4列的拼圖,檢驗是否規律成立 public class Main { public static void main(String[] args) { new Main(); } int a[]; int fac[] = new int[9]; void init() { fac[0] = 1; for (int i = 1; i < 9; i++) fac[i] = fac[i - 1] * i; a = new int[fac[8]]; for (int i = 0; i < a.length; i++) a[i] = -1; } //將一個狀態數值解析成數組,使用全排列散列 int[] toArray(int x) { int ans[] = new int[8]; boolean used[] = new boolean[8]; for (int i = 0; i < 8; i++) { int ind = x / fac[7 - i]; int k; for (k = 0; k < 8; k++) { if (used[k] == false) { ind--; if (ind < 0) break; } } ans[i] = k; used[k] = true; x %= fac[7 - i]; } return ans; } //將狀態數組用全排列散列映射為一個數字 int fromArray(int[] a) { int ans = 0; boolean used[] = new boolean[8]; for (int i = 0; i < 8; i++) { int cnt = 0; for (int k = 0; k < a[i]; k++) { if (used[k] == false) cnt++; } used[a[i]] = true; ans += cnt * fac[7 - i]; } return ans; } // 獲取一個狀態的逆序數,統計后面比我小的個數,這等價於統計后面比我大的個數 int getReverse(int[] a) { int ans = 0; for (int i = 0; i < a.length; i++) { if (a[i] == 0) continue; for (int j = i + 1; j < a.length; j++) { if (a[j] != 0 && a[j] < a[i]) ans ^= 1; } } return ans; } // 獲取一個狀態的逆序數 int getReverse(int x) { int[] a = toArray(x); return getReverse(a); } // 交換,x處為空位,y處為數字 void swap(int[] a, int x, int y) { a[x] = a[y]; a[y] = 0; } public Main() { init(); int start = fromArray(new int[]{1, 2, 3, 4, 5, 6, 7, 0}); Queue<Integer> q = new LinkedList<>(); q.add(start); a[start] = start; while (!q.isEmpty()) { int now = q.poll(); //在狀態轉換中,如果能不把它拆成數組直接產生子狀態效率更高,但實現要麻煩 int[] ar = toArray(now); int i; for (i = 0; i < ar.length; i++) { if (ar[i] == 0) break; } //與其上面的交換位置 if (i - 4 >= 0) { swap(ar, i, i - 4); int s = fromArray(ar); if (a[s] == -1) { a[s] = now; q.add(s); } swap(ar, i - 4, i); } //與下面的交換位置 if (i + 4 < 8) { swap(ar, i, i + 4); int s = fromArray(ar); if (a[s] == -1) { a[s] = now; q.add(s); } swap(ar, i + 4, i); } //與左面交換位置 if (i % 4 != 3) { swap(ar, i, i + 1); int s = fromArray(ar); if (a[s] == -1) { a[s] = now; q.add(s); } swap(ar, i + 1, i); } //與右面交換位置 if (i % 4 != 0) { swap(ar, i, i - 1); int s = fromArray(ar); if (a[s] == -1) { a[s] = now; q.add(s); } swap(ar, i - 1, i); } } for (int i = 0; i < a.length; i++) { int[] ar = toArray(i); if (getReverse(ar) + pos(ar) == 1 && a[i] == -1) { System.out.println(i); } } } // 空位所在的行號奇偶性 int pos(int[] a) { for (int i = 0; i < 4; i++) if (a[i] == 0) return 0; return 1; } }