小清新签到题(DP)
然鹅我并不觉得这道题很清新rua
思维巧妙!(参考)
对于第k小,我们可以这样考虑,若是第k小,那么比它小的方案应该是有k-1个。
在排列组合中,若固定i放在j位置,方案数是确定的,即:i固定在j位置,满足这个条件的序列的rank是在一个范围内的。
对于逆序对
常见思考方式是从小到大枚举数字,考虑对逆序对个数做出的贡献(从小到大插入,后插入不会对前插入造成影响)
设f[i][j]为i个数,j个逆序对的方案数,可得转移方程为
f[i][j]=f[i-1][j]+f[i-1][j-1]+……+f[i-1][j-i+1]
(就相当于给一个比序列中所有数都大的数,让你往里插。)
可用前缀和优化(注意是一层一层的前缀和而不是二维前缀和)
然后我们可以一个个从后往前放数,枚举每一位可以放什么数(1~n),统计放这个数所造成的贡献num,以及之前可以放的数(但因为达不到k而被放弃)的方案数tmp。(但tmp在算这一次放的数是否达到逆序对要求时要加上)
注意f数组容易爆LL,但k的范围是1e13,所以超过了就改为k+1即可。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h> #define LL long long #define N 303 using namespace std; LL read() { LL x=0,f=1;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} return x*f; } LL f[N][N*N],s[N][N*N];//f[i][j]i个数j个逆序对的方案数,s前缀和 int ans[N],vis[N]; void init(LL n,LL k,LL x) { f[0][0]=1; for(int i=0;i<=x;++i)s[0][i]=1;//注意逆序对数可以取到0 for(int i=1;i<=n;++i) { for(int j=0;j<=x;++j) { int l=max(0,j-i+1),r=j; LL tmp=(l==0)?s[i-1][r]:(s[i-1][r]-s[i-1][l-1]); f[i][j]=min(tmp,k+1); s[i][j]=f[i][j]+s[i][j-1]; } } } int main() { LL n=read(),k=read(),x=read(); init(n,k,x); // for(int i=1;i<=n;++i) // for(int j=1;j<=x;++j) // printf("%lld\n",f[i][j]); for(int i=n;i>=1;--i) { LL tmp=0; for(int j=1;j<=n;++j) { if(vis[j])continue; int num=j-1;//对逆序对个数最多产生的贡献,也就是比它小的都在它后面这种情况 for(int l=1;l<j;++l)num-=vis[l]; if(tmp+f[i-1][x-num]>=k) { ans[n-i+1]=j;//可以确定这个位置填j vis[j]=1;x-=num;k-=tmp; break; } tmp+=f[i-1][x-num];//!!! } } for(int i=1;i<=n;++i) printf("%d ",ans[i]); }