ZOJ3874 Permutation Graph(DP+分治NTT)

本文介绍ZOJ3874排列图问题的解决思路与代码实现,通过对逆序对形成的图进行分析,得出联通块的性质,并通过动态规划与分治NTT算法高效计算排列数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ZOJ3874 Permutation Graph

原题地址http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5482

题意:
多组数据。
对于一个排列{a1,a2,… an},如果他把每一对(ai,aj)满足 i< j 且 ai>aj(即逆序对)连接起来,会得到一张图。
例如,如果排列是{2,3,1,4},则1和2连接,1和3连接。
给出得到的图的联通块个数、大小和每个联通块包含哪些数,问可以得到这些联通块的排列有多少种。

数据范围
n <= 130000

题解:
可以推得:
一个联通块只能是一段连续的数字。
块与块之间不存在逆序对的方案是唯一的,就是从小到达排列。

于是转化成每个联通块的方案数之积。

由于方案数只和长度有关,因此可以考虑DP。

最开始陷入了一个思维误区,总是想从dp[i-1]推dp[i],这样考虑加入一个最大/最小的数,反而没什么思路。

实际上枚举的i个点不联通的情况就是之前推导的块间不联通的情况,同样一个联通块内的还是只能连续,且块与块从小到大排列(i< j 则 max(i)< min(j))。

那么考虑用总共的减去不联通的,枚举第一块(1所属的块)大小就行了:
(前面是 1...j 1... j 的联通方案,后面数比j大,肯定是没有边的,所以直接就 (ij)! ( i − j ) !

fi=i!j=1i1fj(ij)! f i = i ! − ∑ j = 1 i − 1 f j ∗ ( i − j ) !

后面是一个卷积的形式,于是采用分治NTT求解即可。

代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int mod=786433; //3*2^18+1 g=10
const int N=100500;
const int MXN=262144+100;
const int inf=0x3f3f3f3f;
int T,n,m,fac[N],a[MXN],b[MXN],w[MXN],_w[MXN],R[MXN],iv[MXN],G=10,f[N];
int modpow(int A,int B)
{
    int ret=1; int base=A;
    for(;B;B>>=1)
    {
        if(B&1) ret=(1LL*ret*base)%mod;
        base=(1LL*base*base)%mod;
    }
    return ret;
}
void NTT(int *x,int opt,int len)
{
    for(int i=0;i<len;i++) if(i<R[i]) swap(x[i],x[R[i]]);
    for(int m=2;m<=len;m<<=1)
    {
        int l=m/2; int wn=(opt==1)?w[m]:_w[m];
        for(int j=0;j<len;j+=m)
        for(int i=0,wi=1;i<l;i++)
        {
            int y=(1LL*wi*x[i+j+l])%mod;
            x[i+j+l]=(x[i+j]-y+mod)%mod;
            x[i+j]=(x[i+j]+y)%mod;
            wi=(1LL*wi*wn)%mod;
        }
    }
    if(opt==-1) for(int i=0;i<len;i++) x[i]=(1LL*x[i]*iv[len])%mod;
}
void solve(int lf,int rg)
{
    if(lf==rg){f[lf]=(fac[lf]-f[lf]+mod)%mod; return;}
    int mid=(lf+rg)>>1;
    solve(lf,mid);
    int len=1,p=0;
    for(;len<2*(rg-lf+1);len<<=1,p++); 
    for(int i=0;i<=len;i++) a[i]=b[i]=0;
    R[0]=0; for(int i=1;i<len;i++) R[i]=(R[i>>1]>>1)|((i&1)<<(p-1));
    for(int i=lf;i<=mid;i++) a[i-lf+1]=f[i];
    for(int i=1;i<=rg-lf;i++) b[i]=fac[i];
    NTT(a,1,len); NTT(b,1,len);
    for(int i=0;i<len;i++) a[i]=(1LL*a[i]*b[i])%mod;
    NTT(a,-1,len);
    for(int i=mid+1;i<=rg;i++) f[i]=(f[i]+a[i-lf+1])%mod;
    solve(mid+1,rg);
}
int main()
{
    fac[0]=1; for(int i=1;i<=100000;i++) fac[i]=(1LL*fac[i-1]*i)%mod;
    for(int i=1;i<MXN;i<<=1) w[i]=modpow(G,(mod-1)/i),_w[i]=modpow(w[i],mod-2),iv[i]=modpow(i,mod-2);
    solve(1,100000);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m); bool flag=0; int ret=1;
        for(int i=1,mx,mn,sz,x;i<=m;i++)
        {
            scanf("%d",&sz); mx=0; mn=inf;
            for(int j=1;j<=sz;j++) {scanf("%d",&x); mn=min(mn,x); mx=max(mx,x);}
            if(mx-mn+1!=sz) flag=1;
            ret=(1LL*ret*f[sz])%mod;
        }
        if(flag) printf("0\n");
        else printf("%d\n",ret);
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值