• 拼图可解的充要条件


    拼图问题又叫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;
        }
    }
  • 相关阅读:
    使用Selenium对付一个点击游戏
    使用Selenium登录新浪微博
    LeetCode题解 #155 Min Stack
    LeetCode题解 #2 Add Two Numbers
    django for monkey(chapter one)
    Django,数据模型创建之数据库API参考(转载)
    python djang suit模板
    Jmeter多机并发压测IP地址问题
    Jmeter进行数据库压测
    fiddler实现手机端抓包(代理)
  • 原文地址:https://www.cnblogs.com/weiyinfu/p/5911340.html
Copyright © 2020-2023  润新知