博客地址 Nicoppa的个人博客
你一定玩过八数码游戏,它实际上是在一个 $3×3$ 的网格中进行的,$1$ 个空格和 $1∼8$ 这 $8$ 个数字恰好不重不漏地分布在这 $3×3$ 的网格中。
例如:
5 2 8
1 3 _
4 6 7
在游戏过程中,可以把空格与其上、下、左、右四个方向之一的数字交换(如果存在)。
例如在上例中,空格可与左、上、下面的数字交换,分别变成:
5 2 8 5 2 _ 5 2 8
1 _ 3 1 3 8 1 3 7
4 6 7 4 6 7 4 6 _
奇数码游戏是它的一个扩展,在一个 $n×n$ 的网格中进行,其中 $n$ 为奇数,$1$ 个空格和 $1∼n^2−1$ 这 $n^2−1$ 个数恰好不重不漏地分布在 $n×n$ 的网格中。
空格移动的规则与八数码游戏相同,实际上,八数码就是一个 $n=3$ 的奇数码游戏。
现在给定两个奇数码游戏的局面,请判断是否存在一种移动空格的方式,使得其中一个局面可以变化到另一个局面。
输入格式
多组数据,对于每组数据:
第 $1$ 行输入一个整数 $n$,$n$ 为奇数。
接下来 $n$ 行每行 $n$ 个整数,表示第一个局面。
再接下来 $n$ 行每行 $n$ 个整数,表示第二个局面。
局面中每个整数都是 $0∼n^2−1$ 之一,其中用 $0$ 代表空格,其余数值与奇数码游戏中的意义相同,保证这些整数的分布不重不漏。
输出格式
对于每组数据,若两个局面可达,输出 TAK
,否则输出 NIE
。
数据范围
$1≤n<500$
输入样例:
3
1 2 3
0 4 6
7 5 8
1 2 3
4 5 6
7 8 0
1
0
0
输出样例:
TAK
TAK
解析
首先一眼题,很简单的办法便是爆搜,终止条件我们只需要看空格位置是否重合,如果重合我们便与答案局面对照,如果相同则可以到达,不同则继续搜索,最后如果没有符合条件的答案便表示不能到达。
部分代码如下
if(x + 1 <= n * n && flag != 1) {
std::swap(a[x], a[x + 1]);
dfs(a, x + 1);
std::swap(a[x], a[x - 1]);
} if(x - 1 >= 1 && flag != 1) {
std::swap(a[x], a[x - 1]);
dfs(a, x - 1);
std::swap(a[x], a[x + 1]);
} if(x + (n - 1) <= n * n && flag != 1) {
std::swap(a[x], a[x + (n - 1)]);
dfs(a, x + (n - 1));
std::swap(a[x], a[x - (n - 1)]);
} if(x - (n - 1) >= 1 && flag != 1) {
std::swap(a[x], a[x - (n - 1)]);
dfs(a, x - (n - 1));
std::swap(a[x], a[x + (n - 1)]);
}
很明显这道题并不能用这个,我的爆搜时间复杂度为 $O(n^3)$ ,且这道题还是多道数据
在二维上解决不了,考虑将二维转一维,由题中样例可得转化后的一维图为
5 2 8
1 3 _
4 6 7
5 2 8 1 3 _ 4 6 7
此样例无法向右交换,但我们可以得到它的其他三种交换后状态 分别为
原 5 2 8 1 3 _ 4 6 7
左 5 2 8 1 _ 3 4 6 7
上 5 2 _ 1 3 8 4 6 7
下 5 2 8 1 3 7 4 6 _
根据向 左 的移动状态可以知道,在空格移动后原数据的相对位置不变,同理 向右也如此。
根据向 上下 的移动状态可以知道,在空格移动后部分原数据的相对位置发生改变,改变的数量为 $n$ 个
从目前是看不出任何东西的,继续思考。此问题为一种 有解性判定 若只考虑输入数列而不考虑结果数列可能并不能得到答案
于是考虑样例
输入局面
1 2 3 _ 4 6 7 5 8
结果局面
1 2 3 4 5 6 7 8 _
易得,想要从输入局面到结果局面 将空格先向右交换然后向下交换,最后再向右交换
分别为
1 2 3 4 _ 6 7 5 8
1 2 3 4 5 6 7 _ 8
1 2 3 4 5 6 7 8 _
空格对数的交换对数列造成了哪些影响?左右交换是改变了一个数的位置,但对一整段数列而言这一个数的相对位置并没有改变,而上下交换却影响的 $n$ 个数的相对位置,有什么办法能快速记录这 $n$ 个数的影响吗?我们发现,根据题意整段数列不存在相同的数,于是我们可以通过记录 逆序对 的方法,来记录这 $n$ 个数的影响。
比如
1 2 3 4 _ 6 7 5 8 改变的逆序对个数为 0
1 2 3 4 5 6 7 _ 8 改变的逆序对个数为 -2
1 2 3 4 5 6 7 8 _ 改变的逆序对个数为 0
可以发现,左右移动不会使逆序对的个数改变,而上下移动会对逆序对的个数改变,因为数列的长度始终为奇数,所以空格每次交换,在数列上的表现一定是移动奇数个单位,从而使 $n - 1$ 个数与空格交换的数的相对位置发生改变。
因此,我们就可以将这 $n$ 个数的各种情况举例出来,看是否存在某种规律。跟上面同理,我们将一段 $n = 3$ 的数列的空格向上移动,就有
a b c d e _ g h i
c d e _
_ d e c
前面是数列各数的关系,后面是逆序对改变前和改变后的数量
c > d > e 3 1
c > e > d 2 0
d > c > e 2 2
d > e > c 1 3
e > d > c 0 2
e > c > d 1 1
我们发现,除了两个逆序对相等的情况,其他都改变了 2 个。
同样我们也可以推出 $n = 5$ 的情况
q w e r t
y u i o p
a s _ f g
h j k l z
x c v b n
q w e r t y u i o p a s _ f g h j k l z x c v b n 原
q w e r t y u i o p a _ s f g h j k l z x c v b n 左
q w e r t y u i o p a s f _ g h j k l z x c v b n 右
q w e r t y u _ o p a s i f g h j k l z x c v b n 上
向上移动,部分变成了
o p a s i
i o p a s
前面是数列各数的关系,后面是逆序对改变前和改变后的数量
i > o > p > a > s 6 10
i > o > p > s > a 5 9
i > o > a > p > s 5 9
i > o > a > s > p 4 8
i > o > s > a > p 3 7
i > o > s > p > a 4 8
(因为太多 只列举部分,但这个部分可能会产生误导)
(从这里可能会误导,|结果数列的逆序对数量-输入数列的逆序对数量| = n - 1,所以我特地找了个数据)
a > p > o > i > s 6 4
可以发现,每次向上(下)移动后逆序对的个数改变都为偶数个,因为我们将空格移动后,将会改变奇数个数的相对位置,但若看成将某个数移动到空格的位置上,就相当于这个数与前面 $n - 1$ 个数交换了位置,因为 $n - 1$ 为偶数,所以逆序对的变化也一定为偶数,为什么?看 $n = 3$ 时举的例子,同样,当 $n > 3 且 n\mod 2≠0$ 时,都可以简化为 $n = 3$ 的情况
因此我们可以得到,向左右移动时,数列的逆序对数量不变,向上下移动时,数列的逆序对奇偶性不变,若结果数列的逆序对个数的奇偶性与输入数列的逆序对个数的奇偶性相同,则说明可以到达,反之则不能,这句话也可以换个意思,由我们模拟的样例可以得到,假如想要从结果数列到达输入数列,那么必定可以得到 |结果数列的逆序对个数 - 输入数列的逆序对个数| Mod 2 = 0 ,于是我们只需要得到这两个数列的逆序对个数就可以得到我们最终的答案。
最后要注意逆序对的个数要开long long,最坏情况为递减数列,即最大逆序对的个数为 $2n^2$ 即为 $250000^2$ 会爆int(但此题的数据并没有卡这个)
代码如下
#include <cstdio>
#include <iostream>
const int MAXN = 500 * 500 + 5;
int arr[MAXN], brr[MAXN], b[MAXN];
long long cnt = 0;
void mergeSort(int a[MAXN], int l, int mid, int r) {
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) {
if(j > r || i <= mid && a[i] <= a[j]) b[k] = a[i++];
else b[k] = a[j++], cnt += mid - i + 1;
} for (int k = l; k <= r; k++) a[k] = b[k];
}
void merge(int l, int r, int a[MAXN]) {
if(l < r) {
int mid = (l + r) >> 1;
merge(l, mid, a);
merge(mid + 1, r, a);
mergeSort(a, l, mid, r);
}
}
int main() {
int n;
while(scanf("%d", &n) != EOF) { //多组数据的输入方式
int kx = 0, ky = 0;
for (int i = 1; i <= n * n; i++) {
int x = 0;
scanf("%d", &x);
if(x != 0) arr[++kx] = x;
}
for (int i = 1; i <= n * n; i++) {
int x = 0;
scanf("%d", &x);
if(x != 0) brr[++ky] = x;
}
if(n == 1) { puts("TAK"); continue; }
long long ans_arr = 0, ans_brr = 0;
merge(1, kx, arr);
ans_arr = cnt, cnt = 0;
merge(1, ky, brr);
ans_brr = cnt, cnt = 0;
if((ans_arr & 1) != (ans_brr & 1)) puts("NIE");
else if(std::abs(ans_brr - ans_arr) % 2 == 0) puts("TAK");
else puts("NIE");
} return 0;
}
此题我重新编写了题解,下面的评论为过时评论,请注意时效性
那比如我们怎么去证明:任何逆序对数量相同的数列都可相同转化
再比如怎么证明:图中某一行末尾和下一行第一个数是不能直接交换的,那我们有什么办法可以实现这种交换呢
最大逆序对的个数给错了。
a b c d e _ g h i
c d e
d e c
前面是数列各数的关系,后面是逆序对改变前和改变后的数量
c > d > e 0 2 这个是不是错了 应该改成c > d > e 3 1
仔细读题解哦 c > d > e 0 2 的意思是
c d e 这个序列 0个逆序对
d e c 这个序列 2个逆序对 d c 和 e c
谢谢纠错 确实错了
说吧你到底是怎么想到的,太NB了,orz
y总提高课讲过hhh
请问一下为什么不能加0进去呢?
因为要算逆序对 0加进去会影响逆序对数量
结论的充分性还是没有证明过啊
nb
博客地址HaloCat挂了
最近回坑了 会补的 十分抱歉
没错,就是出题人的错,但是我还是觉得应该严谨一点
你代码没有long long 求出来的逆序对有些是负数,能AC只是巧合
针对这个问题 我早在 秦淮岸灯火阑珊 dalao 的下面已经看到了,细心点的自己会发现并修改,我不希望自己的题解100%正确,至于为什么cnt要用long long我希望Oier能够自己去考虑(其实我也不知道xxx)
因本人太懒,不是太想改了,如果有细心的就改一下吧。针对这个巧合,我推荐你以后别这么说,你以后可以说 这个数据太水了你才能过的 巧合在Noip上也是一种实力 (所以这种事情应该推在出题人的锅上QAQ
嘛 就是这样
用long long的原因是,对于一个数列,逆序对最多的情况是该数列从大到小排序,则逆序对数量为(n-1) + (n-2) + ....+1,最后的结果为关于 一个int类型的数值 “n” 的 平方级别的数量级。所以要用long long
用 ll 的原因是,500*500 = 25w,逆序对最多会 25w^2 爆 int(soul?)
只要奇偶性就可以了
只要奇偶性就可以了
爆了也没关系
同意
大佬讲的非常清楚,感谢