【DP】ICPC Pacific Northwest Regional Contest 2019 - F - Carny Magician

题目链接https://www.jisuanke.com/contest/8291?view=challenges


题意

询问长度为 n n n ,满足 m m m 个下标与数值匹配的,字典序第k小的排序。


题解

大概也就写了一万个 d p dp dp 吧。
c [ i ] [ j ] c[i][j] c[i][j] 表示 i i i 个位置挑出 j j j 个的方案数,也就是组合数。
f a c [ i ] fac[i] fac[i] 表示 i i i 的阶乘。
f [ i ] f[i] f[i] 表示 i i i 个位置的错排方案数。
g [ i ] [ j ] g[i][j] g[i][j] 表示 i i i 个位置, j j j 个数字不与任何下标匹配的错排方案数。
d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 表示 i i i 个位置, j j j 个位置应该匹配, k k k 个数字不与任何下标匹配的方案数。

前面三个很容易算出,第四个我的转移比较复杂:
i i i 个位置, j j j 个数字不与任何下标匹配(我们称作通配符),也就是说另外有 i − j i-j ij 个数字存在对应下标匹配(称作非通配符)

  • 首先枚举 k k k ,表示从通配符中选出 k k k 个放在非通配符的位置上: c [ j ] [ k ] c[j][k] c[j][k]
  • 非通配符中选出 k k k 个位置放通配符: c [ i − j ] [ k ] c[i-j][k] c[ij][k]
  • 再从这 k k k 个非通配符数挑出 l l l 个放在通配符位置上: c [ k ] [ l ] c[k][l] c[k][l]
  • 同样的也要从没被选中的非通配符中挑出 k − l k-l kl 个放在通配符位置上: c [ i − j − k ] [ k − l ] c[i-j-k][k-l] c[ijk][kl]
  • 现在在通配符位置上的 j j j 个数全排列(原本就在通配符上的通配符,和换进来的非通配符一起全排列): f a c [ j ] fac[j] fac[j]
  • 换出去的通配符也可以在挑出来的 k k k 个位置上全排列: f a c [ k ] fac[k] fac[k]
  • 现在我们已经确立了 j + k j+k j+k 个位置,还剩 i − j − k i-j-k ijk 个。其中原本 i − j i-j ij 个非通配符有 k − l k-l kl 个被占了位置,它们也变成了新的通配符。所以转移到 g [ i − j − k ] [ k − l ] g[i-j-k][k-l] g[ijk][kl]

有关 g g g 的全部转移就在这了:

g[i][j]+=c[j][k]*c[i-j][k]*c[k][l]*c[i-j-k][k-l]*fac[j]*fac[k]*g[i-j-k][k-l];

然后是 d p dp dp 的转移:
这个简单一些,从 i − k i-k ik 个非通配符中挑出 j j j 个满足匹配,剩下就从 g g g 转移:

dp[i][j][k]=c[i-k][j]*g[i-j][k];

有了这个 d p dp dp 之后,只需要每一位一个个枚举,根据方案数来填数字即可。还有如果方案过大,直接设置成无穷就好。


#include<bits/stdc++.h>
using namespace std;
typedef double db;
typedef long long ll;
const int N=3e5+7;
const ll inf=1e18+1e17;
ll c[60][60];
ll f[60];
ll fac[60];
ll dp[60][60][60];
ll g[60][60];
ll n,m,k;
bool vis[60];
int ans[60];
int main()
{
    for(int i=0;i<=50;i++){
        for(int j=0;j<=i;j++){
            if(i==j||j==0) c[i][j]=1;
            else c[i][j]=c[i-1][j-1]+c[i-1][j];
        }
    }
    f[0]=f[2]=1;
    for(int i=3;i<=50;i++){
        if(i<=20) f[i]=f[i-1]*(i-1)+f[i-2]*(i-1);
        else f[i]=inf;
    }
    fac[0]=1;
    for(ll i=1;i<=50;i++){
        if(i<=21) fac[i]=fac[i-1]*i;
        else fac[i]=inf;
    }
    for(int i=0;i<=50;i++){
        for(int j=0;j<=i;j++){
            if(j==0){g[i][j]=f[i];continue;}
            for(int k=0;k<=j&&k<=i-j;k++){
                for(int l=max(0,-i+j+k+k);l<=k;l++){
                    ///挑k个出去,外面挑k个位置,外面位置对应的数中挑l个换进来,外面没被挑中的另外挑k-l个进来,进来的都可以全排列,外面位置全排列放通配符
                    if(inf/c[j][k]/c[i-j][k]/c[k][l]/c[i-j-k][k-l]/fac[j]/fac[k]<g[i-j-k][k-l]) g[i][j]=inf;
                    else if(g[i][j]+c[j][k]*c[i-j][k]*c[k][l]*c[i-j-k][k-l]*fac[j]*fac[k]*g[i-j-k][k-l]>inf) g[i][j]=inf;
                    else g[i][j]+=c[j][k]*c[i-j][k]*c[k][l]*c[i-j-k][k-l]*fac[j]*fac[k]*g[i-j-k][k-l];
                }
            }
        }
    }
    for(int i=0;i<=50;i++){
        for(int j=0;j<=i;j++){
            for(int k=0;k<=i-j;k++){
                if(inf/c[i-k][j]<g[i-j][k]) dp[i][j][k]=inf;
                else dp[i][j][k]=c[i-k][j]*g[i-j][k];
            }
        }
    }

    scanf("%lld%lld%lld",&n,&m,&k);
    if(dp[n][m][0]<k){printf("-1\n");return 0;}
   /// k--;
    int fix=m,wn=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(vis[j]) continue;
            if(fix==0&&i==j) continue;
            int nwn=wn;
            int nfix=fix;
            if(i==j) nfix--;
            else if(!vis[i]) nwn++;
            if(j<i) nwn--;
            ///printf("i=%d  j=%d  wn=%d   nwn=%d   fix=%d   nfix=%d   k=%lld  dp=%lld\n",i,j,wn,nwn,fix,nfix,k,dp[n-i][nfix][nwn]);
            if(k>dp[n-i][nfix][nwn]) k-=dp[n-i][nfix][nwn];
            else{
                fix=nfix;
                wn=nwn;
                ans[i]=j;vis[j]=true;
                break;
            }
        }
    }
    for(int i=1;i<=n;i++) printf("%d%c",ans[i],i==n?'\n':' ');
    printf("\n");

}
/**
50 0 1000000000000000000
48 0 1000000000000000000
*/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值