P1088 [NOIP2004 普及组] 火星人 多种解法(库函数,手写实现库函数,康拓展开+变进制数)

题目描述

人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学家,科学家破解这个数字的含义后,再把一个很小的数字加到这个大数上面,把结果告诉火星人,作为人类的回答。

火星人用一种非常简单的方式来表示数字――掰手指。火星人只有一只手,但这只手上有成千上万的手指,这些手指排成一列,分别编号为1,2,3…1,2,3…1,2,3。火星人的任意两根手指都能随意交换位置,他们就是通过这方法计数的。

一个火星人用一个人类的手演示了如何用手指计数。如果把五根手指――拇指、食指、中指、无名指和小指分别编号为1,2,3,41,2,3,41,2,3,4555,当它们按正常顺序排列时,形成了555位数123451234512345,当你交换无名指和小指的位置时,会形成555位数123541235412354,当你把五个手指的顺序完全颠倒时,会形成543215432154321,在所有能够形成的120120120555位数中,123451234512345最小,它表示111123541235412354第二小,它表示222543215432154321最大,它表示120120120。下表展示了只有333根手指时能够形成的666333位数和它们代表的数字:

三进制数

123123123
132132132
213213213
231231231
312312312 321321321

代表的数字

111
222
333
444
555
666

现在你有幸成为了第一个和火星人交流的地球人。一个火星人会让你看他的手指,科学家会告诉你要加上去的很小的数。你的任务是,把火星人用手指表示的数与科学家告诉你的数相加,并根据相加的结果改变火星人手指的排列顺序。输入数据保证这个结果不会超出火星人手指能表示的范围。

输入格式

共三行。
第一行一个正整数NNN,表示火星人手指的数目(1≤N≤100001 \le N \le 100001N10000)。
第二行是一个正整数MMM,表示要加上去的小整数(1≤M≤1001 \le M \le 1001M100)。
下一行是111NNNNNN个整数的一个排列,用空格隔开,表示火星人手指的排列顺序。

输出格式

NNN个整数,表示改变后的火星人手指的排列顺序。每两个相邻的数中间用一个空格分开,不能有多余的空格。

输入输出样例

输入 #1
5
3
1 2 3 4 5
输出 #1
1 2 4 5 3

说明/提示

对于30%的数据,N≤15N \le 15N15

对于60%的数据,N≤50N \le 50N50

对于全部的数据,N≤10000N \le 10000N10000

 

首先我们需要知道一个性质(这个规律很容易看出来):每一个全排列序列对应一个确定的数

题意可转化为:求一个给定全排列序列在m次变化之后得到的新序列

 

两种思路:

1.从指定序列开始向后进行m次全排列

2.将指定序列转换为一个数,对这个数进行+m的操作,再将变化后的数字还原为序列

 

1.

c++自带一个next_permutation()函数,用法为next_permutation(a+1,a+n+1);(和sort差不多),运行一次后会得到a[]中全排列序列的下一个序列并保存在a[]中

for(int i=1;i<=m;++i) next_permutation(ord+1,ord+1+n);

以上过程结束后ord[]中的序列就是答案

 

也可以选择手动模拟该过程,先写一个正常的全排列,然后从指定位置出发往后找m次即可

for(int i=1;i<=n;i++)

    {
        if(flag==0)i=a[step];
        if(s[i]==0)
        {
            s[i]=1;
            a[step]=i;
            dfs(step+1);
            s[i]=0;
        }
    }
其中if(flag==0)i=a[step]; 即为最关键的一步,将当前全排列过程转化为指定序列,其本质是将枚举全排列的i直接转化到对应的a[i],即给定序列,当转化完后,必定会有step>n的边界情况 ,此时令flag++即可从当前序列开始正常的全排列

if(flagx==1)//当找到了目标序列后逐层退回 return; if(step>n) { flag++; if(flag==m+1) { for(int j=1;j<=n;j++) printf("%d ",a[j]); printf("\n"); flagx=1; } return; }

2.
由于每一个全排列序列都对应这一个确定的数,且这个数的大小和序列的字典序大小相同,所以可以考虑先将序列转化为一个数,对这个数进行+m,然后再将其还原为序列

拓展:全排列的字典序大小,以12345和12543为例,我们从第一位挨个比较过去,第一位都是1,第二位都是2,第三位5》3,所以12543的字典序比12345要大

引入一个概念:康拓展开
模拟一下康拓展开的过程,以45231为例
先看第一位4,有三个数{1,2,3}比4要小,以这三个数开头的五位数显而易见的全部小于以4开头的五位数,后面还有四个位置可以放剩下四个数字,所以这些五位数的个数是:3*4!
再看第二位5,有四个数{1,2,3,4}比5要小,但是前面已经放了个4,所以只有剩下三个数,后面还剩下三个位置,所以这些五位数的个数是:3*3!
接下来同理
最后把所有个数相加,再加上自己这一种情况,得到的结果就是当前序列在所有全排列序列中的字典序排名
由此我们也可以得到康拓展开的本质:将一个数组转换成一个数,康拓展开在hash中也有应用
那怎么通过排名和序列长度反推出当前序列是多少呢?
引入一个概念:逆康拓展开
也就是把上面的过程反过来
以54321为例,已知长度是5,排名是120
首先把排名-1,减去自己的情况,得到119
119/(4!)=4....23,说明第一位的数字比4个数字要大,是5
23/(3!)=3....5,说明第二位的数字比3个数字要大,是4
5/(2!)=2....1,说明第三位的数字比2个数字要大,是3
1/(1!)=1....0,说明第四位的数字比一个数要大,是2
最后一个数字就是没有用过的1
由此得到序列54321
但是容易发现,康拓展开是需要算阶乘的,因此对于21!以下的范围是比较好用的,再往上走就需要高精度或其他方法

那么还有什么方法呢?这里引入一个概念:变进制数
以54321为例,第一位有5种选择,第二位就只有四种选择(第一位的数字不能选了),以此类推,不难发现,对于第i位,一共有着n-i+1种选择,那么我们可以认为,对于第i位,他是n-i+1进制的
接下来要做的就是把给定序列转化为一个变进制数,然后对这个数进行+m的操作,再将其还原为序列
以53142为例
第一位的5是{1,2,3,4,5}中的第五个选择,所以是4(对于R进制的一位,这一位的所有数字只能是从0到(R-1),满R就要进位了)
第二位的3是{1,2,3,4}中的第三个选择,所以是2
第三位的1是{1,2,4}中的第一个选择,所以是0
第四位的4是{2,4}中的第二个选择,所以是1
最后一位的2是{2}的第一个选择,所以是0
由此我们得到了一个变进制数42010,用(42010)unknown表示
接下来是+m的操作,我们以+3为例
42010+3=42013
最后一位(1进制)向前进位得到42040
倒数第二位(2进制)向前进位得到42200
第三位(3进制)没有问题,进位结束
此时我们得到了+m之后的结果(42200)unknown
接下来我们把这个数字还原为序列
(42200)unknown
第一位是4,代表着是{1,2,3,4,5}中的第5个选择,所以是5
第二位是2,代表室{1,2,3,4}中的第3个选择,所以是3
第三位是2,代表着{1,2,4}中的第3个选择,所以是4
第四位是0,代表着{1,2}中的第1个选择,所以是1
最后一位自然是没用过的2
这样我们就得到了序列53412
由于每一位的进制和每一位可选择的数字数量是相同的,所以不会出现明明只有3个数字可以选择这一位却出现4这种情况,换个说法,进制数等于选择数


 

posted @ 2021-08-20 15:58  pcpcppc  阅读(335)  评论(0编辑  收藏  举报