「CodeNote」 N数码问题

Posted by Dawn-K's Blog on August 6, 2019

N数码问题

简介

我们先引入一个简单的问题.

首先,这里的拼图游戏是滑块拼图,类似于华容道,游戏者通过移动拼图块将拼图还原为初始形状。关于拼图,常见的有3x3,4x4,多的以至于有16x16不等。一般块数越多拼图越复杂。

15数码问题

如图所示,我们可以用空格与其上下左右相邻的位置的方块进行交换.给你两个局面,问这两个局面能不能互相转化.

分析

这个就是著名的N数码问题,我们为了严谨,在之后的讨论中都是N*M的矩阵.

先上结论:

(状态A忽略空格时求出的逆序对数量+状态A将空格移动到状态B所需的行数*(A的列数%2+1))%2 == (状态B忽略空格时求出的逆序对的数量)%2 如果成立,那么就可以转化.

看着有些复杂

  • 如果列是奇数

    (状态A忽略空格时求出的逆序对数量)%2 == (状态B忽略空格时求出的逆序对的数量)%2

  • 如果列是偶数

    (状态A忽略空格时求出的逆序对数量+状态A将空格移动到状态B所需的行数)%2 == (状态B忽略空格时求出的逆序对的数量)%2

简单证明

参考博客

注:下文提到的”逆序对”无特殊说明都是指不算入空格在内的.

我们发现,在一行内部,无论怎样左右交换,都不会影响整个数组的逆序对数量.

所以我们就只用关心竖直交换给逆序对带来的影响.我们将空格上下移动一行,除了交换的数外,会影响M-1个其他的数的逆序对.

我们假设M-1个其他数中,有p个比当前数(我们称之为cur)小,有q个比cur大.

假设交换之前的逆序对数量是num,假设交换是cur向下交换.


对于M是奇数,M-1是偶数的情况来说

p和q要么都是奇数,要么都是偶数.cur与空格交换之后,cur的逆序对会增加q;然后对于q个数来说,逆序对数不变;对于p个数来说,逆序对数共减少了p;

则总共的变化是 q-p,根据上文的讨论,这是个偶数,所以挪动一次不影响整体逆序对的的奇偶性.


对于M是偶数,M-1是奇数的来说

p和q中有一个是奇数,一个是偶数,则q-p一定是奇数,则一次上下的挪动一定会改变奇偶性,然而我们可以这样想:我们一上来就将A的空格挪至和B相同的位置,然后无论怎么挪动,如果A能挪动到和B局面完全一样的话,这个空格一定会再次回到这个位置,也就是从这个位置出发,再回到这个位置就一定需要经历偶数次挪动(水平也是偶数,竖直也是偶数),所以从奇偶性角度来看,竖直方向的挪动就是A直接挪到B的空格的位置所需的操作


综上所述,仅M是偶数时,才需要考虑竖直方向的挪动 .

HDU 6620

HDU 6620

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<bits/stdc++.h>
#define sc scanf
#define pr printf
#define pb push_back
#define ll long long
#define fi first
#define se second
#define pii pair<long long ,long long >
using namespace std;
#define MAXN 600000+100
int c[MAXN];
int n = 15;

int lowbit(int x) {
    return x & (-x);
}

void add(int x, int y) {
    while (x <= n) {
        c[x] += y;
        x += lowbit(x);
    }
}

ll sum(int x) {
    ll ans = 0;
    while (x > 0) {
        ans += c[x];
        x -= lowbit(x);
    }
    return ans;
}

int a[20];

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        for (int i = 0; i <= 16; ++i) {
            c[i] = 0;
        }
        int x;
        int pos = 1;
        int ans = 0;
        for (int i = 1; i <= 16; ++i) { // 注意这个循环,0不能算入在内
            scanf("%d", &a[i]);
            if (a[i]) {
                add(a[i], 1);
                ans += pos - 1 - sum(a[i] - 1);
                ++pos;
            } else {
                x = (i - 1) / 4;
            }
        }
        int step = 3 - x; // 这就是算竖直方向的挪动次数
        //  printf("%d %d\n", step, ans);
        if ((step + ans) % 2) {
            printf("No\n");
        } else {
            printf("Yes\n");
        }
    }
    return 0;
}