洛谷1338 末日的传说

题目链接

题解

这题其实可以这么考虑。
我们考虑把这个问题缩小范围。
比如$n=5$,在决定了最小的数”$1$”的位置之后,剩下的几个数是$2,3,4,5$,但是他们具体是多少没必要关心,我们只要关心他们的相对大小关系
所以考虑完当前最小的数,算出这个数对答案的贡献,然后减掉这个贡献,
就可以转而解决一个更小的子问题。(即$n\rightarrow n-1$)
回到题目上,要求是求一个有$m$个逆序对的字典序最小的排列。
我们知道一个长度为$n$的排列最多有$\frac {n(n-1)}2$个逆序对,也知道一个排列的逆序对数越多,排列字典序越大。
所以如果当前$m$不比当前的$\frac {(n-2)(n-1)}2$(也就是减少一个数之后的最多的逆序对数)大,
就可以直接把当前的最小数放在最前面,这肯定是最优的。
反之,则考虑最小数的放置位置。
假设当前排列长为$n$,最小数为$a$,则$a$有$n$种放法,放在从左到右第$i$个位置时会生成$i-1$个逆序对
(因为它左边有$i-1$个比他大)。
因为$m$大于$n-1$长度排列最多所能产生的逆序数,所以$a$不可能放在最前面,否则不满足条件。
怎么办呢?想到之前说的逆序对越多字典序越大,我们就必须让剩下的数能构成的逆序对数尽量小,所以$a$要放到最后,这样$m$减少的最多。
放完了$a$,问题就变成了$n-1$和$m-(a$的贡献$)$的子问题,递归求解即可。时间复杂度$O(n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstdio>
using namespace std;
typedef long long ll;
ll n,m,a[50005];
int main(){
scanf("%lld%lld\n",&n,&m);
ll lst=n,fst=1;
for(int i=1;i<=n;i++){
ll t=(ll)(n-i)*(n-i-1)/2;
if(t>=m)a[fst++]=i;
else a[lst--]=i,m-=(lst-fst+1);
}
for(int i=1;i<=n;i++)printf("%d ",a[i]);
return 0;
}