计数 dp 部分例题(十一~十五部分)

十一、矩阵的利用(行列を用いたテクニック)

1. 快速幂(二分累乗)

(1) 推导转移矩阵(行列の導出)

例题:Placing Squares

题解

(2) BM 优化递推(?)(コンパニオン行列の累乗)

(3) 多项式快速幂(多項式の累乗)

将转移矩阵看成乘上一个多项式的形式,则转移的合并可以从 \(O(n^3)\) 优化到 \(O(n\log n)\)。此时如果每个多项式都相同则可以直接使用快速幂,常数允许/模数为 NTT 模数时可以 进一步优化到 \(O(n\log n)\)

2. 行列式优化计数(行列式のテクニック)

LGV 引理

矩阵树定理

十二、忽略小概率事件(小さい確率を無視する)

在用实数形式表示某个概率或期望时,可以将“极小的贡献”忽略不计,以达到更快速的效果。

例题:Ben Toh

题意

有一个变量 \(x\),初始为 \(0\)。现在需要对其执行 \(n\) 次操作:

  • \(2^x\) 的概率将 \(x\) 加上 \(1\)
  • \(1-2^x\) 的概率将 \(x\) 赋为 \(0\)

求第一次操作被执行的期望次数。

解法

考虑 \(dp\)。设 \(dp_{i,j}\) 为在当前 \(x=j\) 时执行 \(i\) 次操作后 \(x\) 的期望值。转移有 \(dp_{i,j}=2^{-j}(dp_{i-1,j+1}+1)+(1-2^{-j})dp_{i-1,0}\),初值显然为 \(\forall j,dp_{0,j}=0\),目标为 \(\sum_{j=0}^n dp_{i,j}\)

考虑在 \(j\) 足够大的时候,\(dp_{i-1,j+1}+1\) 的贡献是可以忽略不计的,对之后的 dp 值的贡献也可以忽略不计,所以只需要维护每个 \(dp_i\) 的前 \(O(\log)\) 项即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=30;
const int maxn=100010;
int n,i,j;
double dp[2][maxl],sm[maxn],pw[maxl];
double w,a,*X=dp[0],*Y=dp[1]; 
int main(){
    for(pw[0]=j=1;j<maxl;++j)
        pw[j]=pw[j-1]*0.5;
    for(i=1;i<maxn;++i){
        for(j=0;j<maxl-1;++j)
            Y[j]=pw[j]*(X[j+1]+1)+(1-pw[j])*X[0];
        Y[maxl-1]=(1-pw[maxl-1])*X[0];
        sm[i]=Y[0]; a=0; swap(X,Y); 
        for(j=0;j<maxl;++j) Y[j]=0;
    }
    for(;;){
        scanf("%d",&n);
        if(!n) return 0;
        printf("%.8lf\n",sm[n]);
    }
    return 0;
}

十三、二项式(二項係数のテクニック)

1. 常用公式(頻出公式集)

  • \(\sum_{i=0}^n\binom ni=2^n\)
  • \(\sum_{i=0}^{\lfloor\frac n2\rfloor}\binom n{2i}=2^{n-1}\)
  • \(\sum_{i=0}^k\binom{n+i}i=\binom{n+i+1}i\)
  • \(\sum_{i=0}^k\sum_{j=0}^l\binom{i+j}i=\binom{k+l+2}{k+1}\)
  • \(\sum_{i=0}^k\binom ni\binom m{k-i}=\binom{n+m}k\)
  • \(\sum_{i=0}^k\binom{n+i}i\binom{m-i}{k-i}=\binom{n+m+1}k\)

2. 转化为路径计数问题/组合数的几何意义(経路数への帰着)

例题1:BBQ Hard

题意

\(\sum_{i=1}^n\sum_{j=i+1}^n\binom{a_i+a_j+b_i+b_j}{a_i+a_j}\)\(n\le 2\times 10^5;a_i,b_i\le 2\times 10^3\)

解法

考虑 \(\binom{a_i+a_j+b_i+b_j}{a_i+a_j}\) 可以看成 \((-a_i,-b_i)\)\((a_j,b_j)\)只向上/向右走时的路径 数量之和,则原来的和式可以看成 \(\frac 12\left(\sum_{i=1}^n\sum_{j=1}^n\binom{a_i+a_j+b_i+b_j}{a_i+a_j}-\sum_{i=1}^n\binom{2a_i+2b_i}{2a_i}\right)\),前半部分为所有 \((-a_i,-b_i)\) 到所有 \((a_i,b_i)\) 路径数量之和,可以 dp 转移。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxx=2010;
const int maxd=maxx<<1;
const int maxv=maxx<<2;
const int maxn=200010;
const int md=1000000007;
int n,i,j,a,b,ans,dp[maxd][maxd];
int x[maxn],y[maxn],fac[maxv],inv[maxv];
inline int Pow(int d,int z){
    int r=1;
    do{
        if(z&1) r=(1LL*r*d)%md;
        d=(1LL*d*d)%md;
    }while(z>>=1);
    return r;
}
inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
    fac[0]=1;
    for(i=1;i<maxv;++i) fac[i]=(1LL*i*fac[i-1])%md;
    inv[maxv-1]=Pow(fac[maxv-1],md-2);
    for(i=maxv-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md;
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%d%d",&a,&b);
        ++dp[maxx-a][maxx-b];
        Add(ans,C(a<<1,(a+b)<<1));
        x[i]=maxx+a; y[i]=maxx+b;
    }
    ans=md-ans;
    for(i=2;i<maxd;++i){
        for(j=2;j<maxd;++j){
            a=dp[i-1][j-1];
            Add(dp[i][j-1],a);
            Add(dp[i-1][j],a);
        }
    }
    for(i=1;i<=n;++i) Add(ans,dp[x[i]][y[i]]);
    printf("%d\n",(1LL*ans*((md+1)>>1))%md);
    return 0;
}

3. 旋转 45 度(45 度回転)

例题2:Don't worry. Be Together

题意

\(n\) 个点,第 \(i\) 个点的横纵坐标为 \((x_i,y_i)\)。起点为原点,每一步可以向上/下/左/右走一步。设 \(f(i)\) 为走 \(T\) 步刚好能到达 \((x_i,y_i)\) 的方案数,求 \(\prod_{i=1}^n f(i)\bmod mod\)\(1\le mod\le 10^9+7;n,T\le 10^5;|x_i|,|y_i|\le 10^6\)

解法

考虑 \(x_i,y_i\ge 0\) 的情况。如果考虑在 \(x/y\) 轴方向正向/反向走了多少步的情况,则和式难以化简。

考虑将整个坐标系逆时针旋转 45 度,将某个点 \((x,y)\) 对应到新坐标系上的 \((x+y,x-y)\),则在原图上向左/上/右/下走一步对应了在新的坐标系上向左下/左上/右上/右下走一步,新的坐标系上每次可以从某个点 \((x,y)\) 走到 \((x\pm 1,y\pm 1)\) 的位置,可以把横纵坐标分别独立出来看,则走 \(T\) 步到新坐标系上的点 \((X,Y)\) (令 \(X,Y\ge 0\))的方案数为 \(\binom{T}{\frac{T-X}2}\binom{T}{\frac{T-Y}2}\)

注意模数不一定为质数,考虑将最后的答案写成 \(\prod_{i=2}i^{c_i}\) 的形式,则上面的组合数可以拆成某个阶乘除以某个阶乘的形式,乘/除某个阶乘等效于对 \(c\) 前缀加/前缀减,最后处理整个答案时将每个 \(i\) 分解质因数则可以将答案处理出来。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
int n,m,i,j,t,x,y,u,w,a=1;
int v[maxn],p[maxn],d[maxn];
long long pw[maxn];
inline int Pow(int d,long long z){
    int r=1;
    do{
        if(z&1) r=(1LL*r*d)%m;
        d=(1LL*d*d)%m;
    }while(z>>=1);
    return r;
} 
int main(){
    for(i=2;i<maxn;++i){
        if(!v[i]) v[i]=p[++t]=i; 
        for(j=1;j<=t;++j){
            u=p[j];
            if(v[i]<u||i*u>=maxn) break;
            v[i*u]=u;
        }
    } 
    scanf("%d%d%d",&n,&t,&m);
    while(n--){
        scanf("%d%d",&x,&y);
        u=x+y; w=x-y;
        if(u<0) u=-u;
        if(w<0) w=-w;
        if(u<w) swap(u,w); 
        if(t<u||(u^t)&1){
            printf("0\n");
            return 0;
        }
        x=(t-u)>>1; y=(t-w)>>1;
        d[t]+=2; --d[x]; --d[y];
        --d[t-x]; --d[t-y];
    }
    x=0;
    for(i=maxn-1;i;--i){
        if(!(x+=d[i])) continue;
        for(u=i;u!=1;){
            w=v[u]; y=0;
            while(v[u]==w) ++y,u/=w;
            pw[w]+=1LL*y*x;
        }
    }
    for(i=2;i<maxn;++i){
        if(!(u=pw[i])) continue;
        a=(1LL*a*Pow(i,u))%m;
    }
    printf("%d\n",a);
    return 0;
}

4. 卡塔兰数(カタラン数)

十四、容斥原理(包除原理)

1. 使用对称性(?)(対称性を用いる場合)

例题1:~K Perm Counting

题意

\(1\sim n\) 的排列 \(p\) 中,\(\forall i\) 满足 \(|p_i-i|\ne K\)\(p\) 的个数。\(n\le 2\times 10^3\)

解法

考虑容斥,计算钦定若干个 \(i\) 满足 \(|p_i-i|=K\) 的对应 \(p\) 的数量。此时可以以下标为左部点,元素为右部点构造二分图,则钦定 \(c\) 个满足 \(|p_i-i|=K\) 的对应方案数即为二分图上大小为 \(c\) 的匹配数量乘以 \((n-c)!\)。考虑该二分图一定可以拆成 \(2\times \min(K,n-K)\) 条链的形式,则对应匹配数量即为在这些链上选取 \(c\) 条边且满足选择的任意两条边不共点的方案数。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2010;
const int md=924844033;
int n,i,j,k,u,v,d,dp[2][maxn<<1][2]; bool s[maxn<<1];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
    scanf("%d%d",&n,&k);
    j=n%k; v=n/k;
    for(i=u=1;i<=k;++i){
        d=v+(i<=j);
        s[u]=1; u+=d;
        s[u]=1; u+=d;
    }
    auto X=dp[0],Y=dp[1];
    X[0][0]=1; --u;
    for(i=1;i<=u;++i){
        for(j=0;j<=i;++j) Add(Y[j][0]=X[j][0],X[j][1]);
        if(!s[i]) for(j=0;j<i;++j) Add(Y[j+1][1],X[j][0]);
        swap(X,Y); memset(Y,0,sizeof(dp[0])); 
    }
    u=0; d=j=1;
    for(i=n;~i;--i){
        Add(v=X[i][0],X[i][1]);
        v=(1LL*v*d)%md; d=(1LL*d*(j++))%md;
        if(i&1) v=md-v; Add(u,v);
    }
    printf("%d\n",u);
    return 0;
}

2. 使用 dp(DP を用いる場合)

例题2

题意

有一个平面直角坐标系,初始位置为 \((0,0)\),每次可以向横/纵坐标走一步,同时有 \(n\) 个点不能走,求走到 \((X,Y)\) 的方案数。\(X,Y\le 10^5,n\le 2\times 10^3\)

解法

首先不可能直接计算如何走得合法的方案数。考虑从 \(n\le 2\times 10^3\) 入手计算不能走得合法得方案数。

显然某个点 \(x\) 能走到另一个不同的点 \(y\) 则一定有 \(y\) 不能走到 \(x\),可以将所有点按照横/纵坐标排序后,每个点只需要考虑之前的点能否到达它。令 \(p(i,j)\) 为从 \(i\) 走到 \(j\) 的方案数,设 \(dp_{i,j}\) 为当前在点 \(i\),经过了 \(j\) 个不能经过的点的方案数,则转移为 \(dp_{i,j}=\sum_{k=1}^{i-1}dp_{k,j-1}p(k,i)\)。显然最后每个 dp 对答案的贡献只和 \(j\) 的奇偶性相关,则可以把奇偶性相同的 \(j\) 的对应 \(dp_{i,j}\) 合并。

3. 对因数的容斥(約数系包除)

例题3

题意

\(n\) 个数,求其所有非空子集的 \(\gcd\) 之和。值域,\(n\le 10^5\)

解法

考虑从大到小判断每个数是多少个非空子集的 \(\gcd\)。显然可以先统计每个数 \(i\) 的倍数个数 \(c_i\),则以其为 公因数 的非空子集为 \(2^{c_i}-1\) 个,但是需要减去 \(i\) 的所有倍数对应子集的个数。

例题4:Rotated Palindromes

题意

对于所有的长为 \(n\),字符集为 \(1\sim k\) 内的整数的回文串 \(a\),在任意次将 \(a\) 的开头的数字移到末尾的操作之后,求能够生成多少种不同的序列。\(n,k\le 10^9\)

解法

考虑将 \(a\) 的最小循环节移动到末尾之后会形成 \(a\) 本身,所以只需要讨论某个最小循环节带来的贡献即可。显然最小循环节为回文串(和反串相等),则需要考虑该回文串能否经过若干次循环移位之后形成新的回文串。设某个最小循环节 \(S\) 长为 \(d\),如果 \(S\) 能够经过 \(i\) 次操作得到最小循环节 \(T\),则 \(S\) 同样经过 \(d-i\) 次操作也会得到 \(T\)(因为 \(S\) 的长为 \(\min(i,d-i)\) 的前缀和后缀为相等的回文子串)。而上述的 \(i\) 唯一,所以当且仅当 \(i=\frac d2\)\(S\) 能变成 \(T\)。综上,对于长为 \(d\) 的最小循环节,如果 \(d\) 为奇数则贡献为 \(d\),否则贡献为 \(\frac d2\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2700;
const int md=1000000007;
int n,k,i,j,a,u,t,s[maxn],c[maxn];
inline int Pow(int d,int z){
    int r=1;
    do{
        if(z&1) r=(1LL*r*d)%md;
        d=(1LL*d*d)%md;    
    }while(z>>=1);
    return r;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
    scanf("%d%d",&n,&k);
    u=sqrt(n);
    for(i=1;i<=u;++i){
        if(!(n%i)){
            s[++t]=i;
            s[++t]=n/i;
        }
    } 
    if(u*u==n) --t; 
    sort(s+1,s+t+1);
    for(i=1;i<=t;++i){
        n=s[i]; u=Pow(k,(n+1)>>1);
        for(j=1;j<i;++j) if(!(n%s[j])) Add(u,c[j]);
        c[i]=md-u; if(!(n&1)) n>>=1; a=(1LL*u*n+a)%md; 
    }
    printf("%d\n",a);
    return 0;
}

十五、“难解”的计数问题(「解けない問題」を見極める)

#P 问题是一类对 NP 问题计数的问题,其中有一些计数问题是 #PC 的(可能对应的判定性问题不是 NPC 问题)(这种计数问题暂时不能找到多项式的解法):

  • 对一个 2-SAT 问题的合法赋值方案计数
  • 对二分图/一般图的最大匹配计数(对平面图的完美匹配的计数是有多项式解法的)
  • 计算矩阵的积和式
  • 对图的拓扑序计数
  • 对图的欧拉回路计数

所以我们在对计数问题求解时,需要尽量避免直接求解上述问题。


您觉得这几篇文章怎么样呢?今日进行探讨的内容只是 dp 优化方法的一小部分,除此之外我的随笔中还介绍有更多的 dp 优化的方法,我作为一名蒟蒻,衷心地欢迎您来博客园参观。

posted @ 2023-01-14 21:16  Fran-Cen  阅读(67)  评论(1编辑  收藏  举报