题目大意
构造一个从 1 到 n 的排列,该排列满足:对于排列中任意两个相邻的数 ,它们的和或差的绝对值是奇质数(除了 2 以外的质数)。
思路
赛后看了一些题解,大部分的思路都是跟我的不一样的。
一、n 较小的情况
当 较小时,比如小于 10 ,可以直接手算答案,或者使用 C++ 中的全排列函数 next_permutation 来生成所有可能的排列,再去判断哪个排列符合题意。
二、n 较大的情况
当 n 较大时,我们很难手算或暴力求解。
我先详细介绍 n 为 3 的倍数时的方法,再推广到一般情况。
1.当 n 为 3 的倍数时
省流版:
1)按照模 3 的结果不同,将数分成三组;
2)第二组倒过来,接到第一组前面,形成新数列;
3)第三组分成两部分,前半部分倒过来接到数列后面,后半部分倒过来接到数列前面。
是不是一头雾水?哈哈,且看下文详解。
为方便讲解,我假设 n = 15 。
(1)将数分成三组
首先,对于 n 个数,按照模 3 的三种结果(1、2、0),将所有数分成 3 组:
第一组 | 1 | 4 | 7 | 10 | 13 |
---|---|---|---|---|---|
第二组 | 2 | 5 | 8 | 11 | 14 |
第三组 | 3 | 6 | 9 | 12 | 15 |
这样,每一组中任意两个相邻的数都相差 3 ,即三组数列各自满足“任意两个相邻的数差的绝对值为奇质数”。
接下来,我们要考虑如何将这三组数拼接起来。
(2)处理一二组
将第二组倒过来,接在第一组的开头:
第二组 第一组
14 11 8 5 2 1 4 7 10 13
这样,不管 多大, 2 和 1 总是接在一起的,它们满足“和为奇质数”。
现在,我们处理了第一组和第二组,拼接起来的新数列满足了题意,还剩第三组。
(3)处理第三组
接下来,是比较难想到的一步,也是我灵光一闪的想法。
将第一组的最后一个数减去 7 ,即 13 - 7 = 6 ;再将第二组的最后一个数减去 5 ,即 14 - 5 = 9。
这时候会惊奇地发现, 6 和 9 均在第三组中出现,且它们相邻。
这是偶然发现的规律,简单解释一下:(感觉有点啰嗦就设成灰色了hh)
第一组的数,模 3 等于 1 ,那么减去 7 ,就是先减去 1 ,变为 3 的倍数,再减去 2 个 3 ;
第二组的数,模 3 等于 2 ,那么减去 5 ,就是先减去 2 ,变为 3 的倍数,再减去 1 个 3 。
注意,这两个数要在同一列。不难发现,第一组的数减去 1 ,和第二组的数减去 2 ,结果是一样的。那么一个减去 2 个 3 ,一个减去 1 个 3 ,就会得到两个相邻的 3 的倍数了。
为什么是减去 7 和 5 呢?当然是因为它们是奇质数啦!
得到了第三组中两个相邻的数 6 和 9 ,我们就在这两个数中间切一刀,将第三组的数分成两部分。
把步骤 2 得到的数列拿过来,第三组的前半部分倒过来接到数列后面,后半部分倒过来接到数列前面:
第三组(后半) 第二组 第一组 第三组(前半)
15 12 9 14 11 8 5 2 1 4 7 10 13 6 3
这样, 9 和 14 相差 5 , 6 和 13 相差 7 ,这两个差都是奇质数,满足题意。
现在,这四段数列各自满足题意 ,三个连接处的三对数也满足题意。至此,构造完成。
2.当 n 不为 3 的倍数时(更一般的情况)
如果 n 不是 3 的倍数,在分三组时就会出现最后一列填不满的情况。
上面 n 为 3 的倍数时的思路是我提出来的,队友也很快举一反三,推广了方法。
(1) n mod 3 = 2
比如 n = 14 :
第一组 | 1 | 4 | 7 | 10 | 13 |
---|---|---|---|---|---|
第二组 | 2 | 5 | 8 | 11 | 14 |
第三组 | 3 | 6 | 9 | 12 |
这时,我们可以照样用上面的方法。虽然第三组的末尾少了一个 15 ,但这并不影响将它分割成两部分。按照上述方法得到的排列长这样:
第三组(后半) 第二组 第一组 第三组(前半)
12 9 14 11 8 5 2 1 4 7 10 13 6 3
跟 n = 15 得到的排列相比,只是在最前面少了一个 15 。
(2) n mod 3 = 1
当 n = 13 时,表格长这样:
第一组 | 1 | 4 | 7 | 10 | 13 |
---|---|---|---|---|---|
第二组 | 2 | 5 | 8 | 11 | |
第三组 | 3 | 6 | 9 | 12 |
这时候就不能照搬上面的方法了,难点在于如何将某一组分成两半。
我们可以将上述方法稍微改变一下:
1)按照模 3 的结果不同,将数分成三组;
2)第三组倒过来,接到第二组前面,形成新数列;
3)通过 11 - 7 = 4 和 12 - 5 = 7,将第一组数列在 4 和 7 中间切开,分成两部分,前半部分倒过来接到数列后面,后半部分倒过来接到数列前面。
最终得到的答案长这样:
第一组(后半) 第三组 第二组 第一组(前半)
13 10 7 12 9 6 3 2 5 8 11 4 1
3.小结
【1】 n mod 3 = 0 或 n mod 3 = 2 时
1)按照模 3 结果的不同,将 n 个数分成三组数列;
2)将第二组数列反转,接到第一组的前面,形成新数列;
3)设 x = 第一组末尾的数减去 7 , y = 第二组末尾的数减去 5 ,然后在第三组数列中找到 x 和 y ,将第三组数列从 x 和 y 中间切成两半,前一半反转接到新数列后面,后一半反转接到新数列前面。
【2】 n mod 3 = 1 时
1)按照模 3 结果的不同,将 n 个数分成三组数列;
2)将第三组数列反转,接到第二组的前面,形成新数列;
3)设 x = 第二组末尾的数减去 7 , y = 第三组末尾的数减去 5 ,然后在第一组数列中找到 x 和 y ,将第一组数列从 x 和 y 中间切成两半,前一半反转接到新数列后面,后一半反转接到新数列前面。
代码实现
上述方法,理解起来不算太难,但代码实现似乎有点难度,于是编程的工作交给了编程能力更强的队长。下面是队长写的、一发就 AC 的代码。
#include <iostream>
#include <cstring>
#define MAXN (int)1e6+5
using namespace std;
int T,n,cnt,a1,a2,a3,ans[MAXN],t;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>T;
while(T--){
cin>>n;
if (n>=7){
t=n%3;
a3=n-t;
a1=(n/3)*3-2+(t?3:0);
a2=(n/3)*3-1+(t==2?3:0);
if (t==1||t==0){
for (int i=1,ed=a2-7;i<=ed;i+=3) cout<<i<<' ';
for (int i=a2;i>0;i-=3) cout<<i<<' ';
for (int i=3;i<=a3;i+=3) cout<<i<<' ';
for (int i=a3-5;i<=n;i+=3) cout<<i<<' ';
}else{
for (int i=3,ed=a1-7;i<=ed;i+=3) cout<<i<<' ';
for (int i=a1;i>0;i-=3) cout<<i<<' ';
for (int i=2;i<=a2;i+=3) cout<<i<<' ';
for (int i=a2-5;i<=n;i+=3) cout<<i<<' ';
}
}else{
cnt=1;
for (int i=n-((n&1)?0:1);i>0;i-=2,cnt+=2) ans[cnt]=i;
cnt=2;
for (int i=2;i<=n;i+=2,cnt+=2) ans[cnt]=i;
for (int i=1;i<=n;i++) cout<<ans[i]<<' ';
}
cout<<'\n';
}
}
我赛时尝试编程的时候,好像用了很多数组哈哈。我还没敲几行,队长就敲完了。有时间我也要试着敲一下。
补充
牛客这么多场比赛,大多数题目都是另外两个队友解出来,而这道题是我提供了思路,感觉挺自豪的~