拼图问题又叫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; } }