随笔- 805  文章- 1  评论- 68 

拼图问题又叫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;
    }
}
posted on 2016-09-27 01:56 weiyinfu 阅读(...) 评论(...) 编辑 收藏