小清新签到题(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即可。

#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
",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]);
} 
View Code
原文地址:https://www.cnblogs.com/yyys-/p/11360156.html