前言
这题一开始被思维定式了,以为用普通的数学方法就能过,后来后知后觉发现这里要是直接用数学方法的话,这个所涉及的整数会达到
10000
!
10000!
10000!。所以不可避免的会WA。然后发现其实这题是个模拟高精度的题,也就是自己模拟高精度计算。虽然不知道为什么会把这种模拟高精度的问题放到暴力搜索专题下。但是这道题目确实还是有点东西的,因此个人感觉还是有必要好好写下题解。
先放题目来源:P1088 [NOIP2004 普及组] 火星人
题意理解:
这题题目描述很长,但是抽象出来的描述是这样的:
假设有一列从1到N的数,这N个数对应着
N
!
N!
N!个全排列,这样的话,按照字典序给这N!个全排列从1到
N
!
N!
N!标记上大小(也可以是从0到
N
!
N!
N!-1,因为本题用到的是相对顺序。)例如,假设对于1-3的三个数,有
3
!
3!
3!=6个全排列,分别是1 2 3(1),1 3 2(2),2 1 3(3),2 3 1(4),3 1 2(5),3 2 1(6),其中(1-6)表示的是其大小顺序。也可以用0-5标记。然后题目还会给出这
N
!
N!
N!个排列中的其中一个排列,例如2 1 3,这对应的大小顺序是3,然后给出一个很小的数M,例如1,3+1=4,而第4顺序的排列对应的全排列为2 3 1。这样,题目输入会给出全排列长度N,以及要加上的这个很小的数M,以及当前已知的全排列顺序,我们要做的就是先将已知的全排列转化成其在全排列里的大小T,然后T+M,对应找到待变换的全排列的顺序然后再逆变换回去成一个全排列输出。
因为对于一个1-N的数来说,其全排列数量有
N
!
N!
N!个,因此,穷举所有可能性是不现实的,计算机字长也支持不下。所以需要通过模拟高精度的方式来解决这个问题。
一个数学基础:
解决这题想用模拟高精度的方法需要知道一个数学基础,康托展开,看百度百科的话可能比较抽象,但实际上,康托展开是很容易理解的一样东西。
假设有这样一个排列
a
1
,
a
2
,
.
.
.
.
a
N
a_1,a_2,....a_N
a1,a2,....aN,并且假设从
a
i
+
1
到
a
N
a_{i+1}到a_N
ai+1到aN一共有
n
i
n_i
ni个数小于
a
i
a_i
ai。例如,对于一列数4 2 1 3 5,因为2 1 3 5中有三个数小于4,所以
n
1
n_1
n1=3,在3 1 5中只有1小于2,所以
n
2
n_2
n2=1,依次类推可以得到这列数对应的
n
1
,
n
2
,
.
.
.
n
N
n_1,n_2,...n_N
n1,n2,...nN为3 1 0 0 0 。这样的话,考虑第1个数
a
1
a_1
a1,因为右边有
n
1
n_1
n1个数小于它,所以对应这一位(也就是最高位)上,取这
n
1
n_1
n1个数中的任何一个都会比取
a
1
a_1
a1小,而剩下N-1个数,无论怎么排列,只要最高位不是
a
1
a_1
a1而是那
n
1
n_1
n1个数中的任何一个,都会小于它,因此对于最高位来说,有
n
1
∗
(
N
−
1
)
!
n_1*(N-1)!
n1∗(N−1)!中小于它的可能;而对于第二位
a
2
a_2
a2来说,同样可以得到当固定
a
1
,
a
2
a_1,a_2
a1,a2后会有
n
2
∗
(
N
−
2
)
!
n_2*(N-2)!
n2∗(N−2)!中排列比它小,依次到
a
N
a_N
aN。这样可以得到下列式子,假设
X
为
a
1
,
a
2
,
a
2
.
.
.
.
a
N
X为a_1,a_2,a_2....a_N
X为a1,a2,a2....aN排列对应的在所以全排列里的顺序(从0开始,具体原因是,考虑
n
1
−
n
N
n_1-n_N
n1−nN全为0,即完全正序,1 2 3 4… N的情况,显然为
0
∗
(
N
−
1
)
!
+
0
∗
(
N
−
2
)
!
+
.
.
.
.
+
0
∗
0
!
=
0
0*(N-1)!+0*(N-2)!+....+0*0!=0
0∗(N−1)!+0∗(N−2)!+....+0∗0!=0)
X
=
n
1
∗
(
N
−
1
)
!
+
n
2
∗
(
N
−
2
)
!
+
.
.
.
+
1
∗
1
!
+
0
∗
0
!
X=n_1*(N-1)!+n_2*(N-2)!+...+1*1!+0*0!
X=n1∗(N−1)!+n2∗(N−2)!+...+1∗1!+0∗0!
这样康托展开就实现了从一组排列向具体顺序的转化。逆转化也是可以的。当知道一个一个数
X
X
X时求原排列也是很轻松的。这就是说,观察上述式子,
X
(
N
−
1
)
!
=
n
1
.
.
.
.
.
.
.
n
2
∗
(
N
−
2
)
!
+
n
3
∗
(
N
−
3
)
!
.
.
.
.
+
0
∗
0
!
\frac{X}{(N-1)!}=n_1.......n_2*(N-2)!+n_3*(N-3)!....+0*0!
(N−1)!X=n1.......n2∗(N−2)!+n3∗(N−3)!....+0∗0!那么一直用
X
(
N
−
i
)
!
\frac{X}{(N-i)!}
(N−i)!X可以取出
n
i
n_i
ni,然后利用
X
=
X
%
(
N
−
i
)
!
X=X\%(N-i)!
X=X%(N−i)!求
n
i
+
1
n_{i+1}
ni+1。这样就可以根据
X
X
X求得
n
1
,
n
2
,
.
.
.
.
.
,
n
N
n_1,n_2,.....,n_N
n1,n2,.....,nN了。这样,就在集合
S
=
{
1
,
2
,
3
,
.
.
.
N
}
S=\{1,2,3,...N\}
S={1,2,3,...N}中,先找第
n
1
+
1
n_1+1
n1+1大的数
n
u
m
1
num_1
num1,然后更新集合
S
=
{
1
,
2
,
3
,
.
.
.
.
N
}
−
n
u
m
1
S=\{1,2,3,....N \}-{num_1}
S={1,2,3,....N}−num1,然后,在新集合
S
S
S中找第
n
2
+
1
n_2+1
n2+1大的数
n
u
m
2
num_2
num2作为次高位的排列,然后依次往下知道
S
=
ϕ
S=\phi
S=ϕ。这样逆康托展开就完成了,也即完成了从顺序到排列的转换。
解题思路:
上述数学基础是为了方便求解才写的。实际上,要解决本题暴力康托展开肯定还是不行的,而需要模拟高精度。这就是说,观察
X
=
n
1
∗
(
N
−
1
)
!
+
n
2
∗
(
N
−
2
)
!
+
.
.
.
+
1
∗
1
!
+
0
∗
0
!
X=n_1*(N-1)!+n_2*(N-2)!+...+1*1!+0*0!
X=n1∗(N−1)!+n2∗(N−2)!+...+1∗1!+0∗0!,由于原题中已经说明,
M
M
M是个很小的正整数,所以
M
M
M其实可以表示成
M
=
m
1
∗
4
!
+
m
2
∗
3
!
+
m
3
∗
2
!
+
m
4
∗
1
!
M=m_1*4!+m_2*3!+m_3*2!+m4*1!
M=m1∗4!+m2∗3!+m3∗2!+m4∗1!的形式,那么,可以用一个比较大的数组记录下
n
1
,
n
2
到
n
N
n_1,n_2到n_N
n1,n2到nN,然后再用个数组记录下
m
1
,
m
2
.
.
.
m
4
m_1,m_2...m_4
m1,m2...m4但是为了方便后面计算我还是把两个数组开的一样大了。只不过把后续没有的置为0。然后,将两个数组对应位数相加,满足进位条件则进位(注意,这里还要考虑进位,我用add来表示)。然后加起来得到新的
n
1
′
,
n
2
′
,
.
.
.
.
.
n
N
′
n_1',n_2',.....n_N'
n1′,n2′,.....nN′,根据康托逆展开再展开回去就行。这样就可以避免大数问题了。下面是我的AC代码:
#include <stdio.h>
#include <stdlib.h>
int a[10010];
int num[10010];
int M_num[10010];
int pre[10010];
int outcome[10010];
int fac(int i){
if(i==0)
return 1;
else
return i*fac(i-1);
}
int main(int argc, char *argv[]) {
int N,M,i,j,temp_num,add=0,temp; //M_num[]记录M按照4! 3! 2! 1!的计数对应数制位数,add表示是否需要进位
scanf("%d%d",&N,&M);
for(i=0;i<N;i++){
scanf("%d",&a[N-1-i]);
pre[i+1]=i+1;}
for(i=N-1;i>=0;i--){
temp_num=0;
for(j=i-1;j>=0;j--){
if(a[j]<a[i])
temp_num++;
}
num[i]=temp_num;
} //a[i]记录这i个数,num[i]记录从a[i+1]开始直到a[N-1]的比a[i]小的数
for(i=4;i>0;i--){
M_num[i]=M/fac(i);
M=M%fac(i);
}
for(i=1;i<=N-1;i++){
if(M_num[i]+num[i]+add>i){
num[i]=(M_num[i]+num[i]+add)%(i+1);
add=1;
}else{
num[i]=M_num[i]+num[i]+add;
add=0;
}
}
//下面从N个数中找对应的数
for(i=N-1;i>=0;i--){
temp=num[i];
for(j=1;j<=N;j++){
if(pre[j]==0)
;
else{
if(temp==0){
outcome[N-1-i]=pre[j];
pre[j]=0;
break;
}else{
temp--;
}
}
}
}
for(i=0;i<N;i++){
printf("%d ",outcome[i]);
}
return 0;
}