【LOJ6077】【2017 山东一轮集训 Day7】逆序对(排列计数DP,容斥,旋转体积背包)

20 篇文章 0 订阅
4 篇文章 0 订阅
这篇博客探讨了两种高效解决排列计数问题的方法,包括动态规划和旋转体积背包技术。博主详细解释了如何利用生成函数和指数运算优化动态规划,以及如何通过旋转体积背包实现O(k√k)的时间复杂度。内容深入浅出,适合对算法感兴趣的读者学习。
摘要由CSDN通过智能技术生成

搞不懂排列计数。

一种计数方法是考虑从小往大往当前排列中插入每个数,存在一次插的位置不同那么最后的结果也不同。

那么考虑插入 i i i 时,对于 x ∈ [ 0 , i − 1 ] x\in[0,i-1] x[0,i1] 均有且仅有一种方法使得逆序对个数增加 x x x

法一:

考虑 DP,设 f i , j f_{i,j} fi,j 表示插入了 i i i 个数,已经有 j j j 个逆序对的方案数,那么有转移 f i , j = ∑ l = 0 i − 1 f i − 1 , j − l f_{i,j}=\sum\limits_{l=0}^{i-1}f_{i-1,j-l} fi,j=l=0i1fi1,jl

暴力转移是 O ( n 2 ) O(n^2) O(n2) 的,考虑使用生成函数优化。设 F i ( x ) F_i(x) Fi(x) f i , j f_{i,j} fi,j 的生成函数,那么:
F i ( x ) = ( 1 + x + ⋯ + x i − 1 ) F i − 1 ( x ) F_i(x)=(1+x+\cdots+x^{i-1})F_{i-1}(x) Fi(x)=(1+x++xi1)Fi1(x)
于是:
F n ( x ) = ∏ i = 1 n ( 1 + x + ⋯ + x i − 1 ) = ∏ i = 1 n 1 − x i 1 − x = ∏ i = 1 n ( 1 − x i ) ( 1 − x ) n F_n(x)=\prod_{i=1}^{n}(1+x+\cdots+x^{i-1})=\prod_{i=1}^{n}\dfrac{1-x^i}{1-x}=\dfrac{\prod\limits_{i=1}^n(1-x^i)}{(1-x)^n} Fn(x)=i=1n(1+x++xi1)=i=1n1x1xi=(1x)ni=1n(1xi)
我们要求的就是上式的 k k k 次项系数。这个式子为多个单项式求积,考虑先取 ln ⁡ \ln ln exp ⁡ \exp exp
ln ⁡ F n ( x ) = ∑ i = 1 n ln ⁡ ( 1 − x i ) − n ln ⁡ ( 1 − x ) \ln F_n(x)=\sum_{i=1}^n\ln(1-x^i)-n\ln(1-x) lnFn(x)=i=1nln(1xi)nln(1x)
其中对 ln ⁡ ( 1 − x a ) \ln(1-x^a) ln(1xa) 求导貌似是个很套路的东西:先求导再积分:
  [ ln ⁡ ( 1 − x a ) ] ′ = − a x a − 1 1 − x a = ∑ i = 0 ∞ − a x a i + a − 1 ln ⁡ ( 1 − x a ) = ∑ i = 0 ∞ − a a i + a x a i + a = ∑ i = 1 ∞ − x a i i \begin{aligned} \ [\ln(1-x^a)]'&=\dfrac{-ax^{a-1}}{1-x^a}=\sum_{i=0}^{\infty}-ax^{ai+a-1}\\ \ln(1-x^a)&=\sum_{i=0}^{\infty}-\dfrac{a}{ai+a}x^{ai+a}=\sum_{i=1}^{\infty}-\dfrac{x^{ai}}{i} \end{aligned}  [ln(1xa)]ln(1xa)=1xaaxa1=i=0axai+a1=i=0ai+aaxai+a=i=1ixai
那么直接对于每个 a a a 枚举 a a a 的倍数即可,时间复杂度是调和级数级别的。

最后再对 ln ⁡ F n ( x ) \ln F_n(x) lnFn(x) exp ⁡ \exp exp k k k 次项即为答案。总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

法二:

即为求下式的解的方案数:
∑ i = 1 n x i = k \sum_{i=1}^n x_i=k i=1nxi=k
其中需要满足 x i ∈ [ 0 , i − 1 ] x_i\in[0,i-1] xi[0,i1]

这个限制不好做,考虑容斥,钦定一些位置不满足:
a n s = ∑ S ⊆ { 1 , ⋯   , n } ( − 1 ) ∣ S ∣ × c ( k − ∑ i ∈ S i ) ans=\sum_{S\subseteq\{1,\cdots,n\}}(-1)^{|S|}\times c\left(k-\sum_{i\in S}i\right) ans=S{1,,n}(1)S×c(kiSi)
解释一下这条式子,枚举对于 i ∈ S i\in S iS 不满足限制,也就是 x i ≥ i x_i\geq i xii,不妨设 y i = x i − i y_i=x_i-i yi=xii,只需满足 y i ≥ 0 y_i\geq 0 yi0 即可,那么对于 i ∈ S i\in S iS y i y_i yi i ∉ S i\not\in S iS x i x_i xi 的限制都只有 ≥ 0 \geq 0 0

于是 c ( k ) c(k) c(k) 的定义也显而易见了: ∑ i = 1 n z i = k \sum\limits_{i=1}^nz_i=k i=1nzi=k 的方案数,其中限制只有 z i ≥ 0 z_i\geq 0 zi0 c ( k ) c(k) c(k) 通过插板法容易得到。

考虑 DP,暴力的想法是考虑完 1 ∼ n 1\sim n 1n 中的数,选了 i i i 个不同的数( ∣ S ∣ = i |S|=i S=i),和为 j j j ∑ p ∈ S p = j \sum\limits_{p\in S}p=j pSp=j)的方案数,尽管 i i i 的取值范围只有 k \sqrt k k ,但时间复杂度还是 O ( n k k ) O(nk\sqrt k) O(nkk ),甚至比暴力还慢。

神奇的方法是使用旋转体积背包:设 g i , j g_{i,j} gi,j 表示从 1 ∼ n 1\sim n 1n 中选 i i i 个不同的数,他们的和为 j j j 的方案数。

每次转移可以考虑集体垫高一层,然后再看是否还要在末尾加上一列:
g i , j → g i , j + i g i , j → g i + 1 , j + i + 1 \begin{aligned} g_{i,j}&\to g_{i,j+i}\\ g_{i,j}&\to g_{i+1,j+i+1} \end{aligned} gi,jgi,jgi,j+igi+1,j+i+1
注意对于算出来的 g i , j g_{i,j} gi,j 要减掉 g i − 1 , j − ( n + 1 ) g_{i-1,j-(n+1)} gi1,j(n+1),这是为了防止选的数大于 n n n 而减掉垫高一层后最高一列为 n + 1 n+1 n+1 后面的列高度 ≤ n \leq n n 的方案( g i , j g_{i,j} gi,j 一定是由某个状态垫高一层转移过来,而转移过来的状态已经保证了所有数小于等于 n n n)。

时间复杂度 O ( k k ) O(k\sqrt k) O(kk )

#include<bits/stdc++.h>

#define SN 450
#define N 100010

using namespace std;

namespace modular
{
    const int mod=1000000007;
    inline int add(int x,int y){return x+y>=mod?x+y-mod:x+y;}
    inline int dec(int x,int y){return x-y<0?x-y+mod:x-y;}
    inline int mul(int x,int y){return 1ll*x*y%mod;}
    inline void Add(int &x,int y){x=x+y>=mod?x+y-mod:x+y;}
    inline void Dec(int &x,int y){x=x-y<0?x-y+mod:x-y;}
}using namespace modular;

inline int poww(int a,int b)
{
    int ans=1;
    while(b)
    {
        if(b&1) ans=mul(ans,a);
        a=mul(a,a);
        b>>=1;
    }
    return ans;
}

inline int read()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        x=(x<<1)+(x<<3)+(ch^'0');
        ch=getchar();
    }
    return x*f;
}

int n,k,g[SN][N];
int fac[N<<1],ifac[N<<1];

int C(int n,int m)
{
    return mul(mul(fac[n],ifac[m]),ifac[n-m]);
}

int calc(int s)
{
    return C(n+s-1,n-1);
}

int main()
{
    n=read(),k=read();
    fac[0]=1;
    for(int i=1;i<=n+k;i++) fac[i]=mul(fac[i-1],i);
    ifac[n+k]=poww(fac[n+k],mod-2);
    for(int i=n+k;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
    g[0][0]=g[1][1]=1;
    for(int i=1;i<=n&&i*(i+1)/2<=k;i++)
    {
        for(int j=i*(i+1)/2;j<=k;j++)
        {
            if(j>=n+1) Dec(g[i][j],g[i-1][j-n-1]);
            if(j+i<=k) Add(g[i][j+i],g[i][j]);
            if(j+i+1<=k) Add(g[i+1][j+i+1],g[i][j]);
        }
    }
    int ans=0;
    for(int i=0;i<=n&&i*(i+1)/2<=k;i++)
    {
        for(int j=i*(i+1)/2;j<=k;j++)
        {
            if(i&1) Dec(ans,mul(g[i][j],calc(k-j)));
            else Add(ans,mul(g[i][j],calc(k-j)));
        }
    }
    printf("%d\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值