P1338 末日的传说

斯德哥尔摩

2018-10-17 00:37:05

Personal

[P1338 末日的传说](https://www.luogu.org/problemnew/show/P1338) 我们考虑把这个问题缩小范围。 比如$n==5$,在决定了最小的数$1$的位置之后,剩下的几个数是$2\ 3\ 4\ 5$,但是具体他们是多少没必要关心,我们只要关心他们的相对大小关系。 所以考虑完当前最小的数,算出这个数对答案的贡献,然后减掉这个贡献,就可以转而解决一个更小的子问题。 即:$n->n-1$ 回到题目上,要求是求一个有$m$个逆序对的字典序最小的排列。 我们知道一个长度为$n$的排列最多有$\frac{n(n-1)}{2}$个逆序对,也知道一个排列的逆序对数越多,排列字典序越大。 所以如果当前$m$不比当前的$\frac{(n-1)(n-2)}{2}$(也就是减少一个数之后的最多的逆序对数)大,就可以直接把当前的最小数放在最前面,这肯定是最优的。 反之,则考虑最小数的放置位置。 假设当前排列长为$n$,最小数为$a$,则$a$有$n$种放法,放在从左到右第$i$个位置时会生成$i-1$个逆序对。 因为它左边有$i-1$个比他大。 因为$m$大于$n-1$长度排列最多所能产生的逆序数,所以$a$不可能放在最前面,否则不满足条件。 怎么办呢? 想到之前说的逆序对越多字典序越大,我们就必须让剩下的数能构成的逆序对数尽量小,所以$a$要放到最后,这样$m$减少的最多。 放完了$a$,问题就变成了$n-1$和$m-(\text{a的贡献})$的子问题,递归求解即可。 时间复杂度$O(n)$。 附代码: ```cpp #include<iostream> #include<algorithm> #include<cstdio> #define MAXN 50010 using namespace std; int n,m,ans[MAXN]; long long head,tail; inline int read(){ int date=0,w=1;char c=0; while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9'){date=date*10+c-'0';c=getchar();} return date*w; } void work(){ n=read();m=read(); head=1;tail=n; for(int i=1;i<=n;i++){ long long x=1ll*(n-i)*(n-i-1)/2; if(x>=m)ans[head++]=i; else{ ans[tail--]=i; m-=(tail-head+1); } } for(int i=1;i<=n;i++)printf("%d ",ans[i]); printf("\n"); } int main(){ work(); return 0; } ```