2023牛客暑期多校训练营8 J题 题解/复盘

J - Permutation and Primes

题目大意

构造一个从 1 到 n 的排列,该排列满足:对于排列中任意两个相邻的数 ,它们的和或差的绝对值是奇质数(除了 2 以外的质数)。

思路

赛后看了一些题解,大部分的思路都是跟我的不一样的。

一、n 较小的情况

当 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

471013
第二组2581114
第三组3691215

这样,每一组中任意两个相邻的数都相差 3 ,即三组数列各自满足“任意两个相邻的数差的绝对值为奇质数”。

接下来,我们要考虑如何将这三组数拼接起来。

(2)处理一二组

将第二组倒过来,接在第一组的开头:

第二组         第一组
14 11 8 5 2   1 4 7 10 13

这样,不管 n 多大, 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 :

第一组1471013
第二组2581114
第三组36912

这时,我们可以照样用上面的方法。虽然第三组的末尾少了一个 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 时,表格长这样:

第一组1471013
第二组25811
第三组36912

这时候就不能照搬上面的方法了,难点在于如何将某一组分成两半。

我们可以将上述方法稍微改变一下:

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';
    }
}

我赛时尝试编程的时候,好像用了很多数组哈哈。我还没敲几行,队长就敲完了。有时间我也要试着敲一下。

补充

牛客这么多场比赛,大多数题目都是另外两个队友解出来,而这道题是我提供了思路,感觉挺自豪的~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Washington2022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值