计数DP

前置芝士

好像也没啥( ̄▽ ̄)"

(组合数还是要会求的)

作用

​ 统计对象的可能情况一般比较多,通常需要精确的划分和整体性的计算。因此,使用动态规划抽象出问题中的“子结构”和推导的“阶段”,将有助于我们准确而高效地进行求解。

​ 这些问题往往都起源于排列组合中的组合公式 (C_n^k=C_{n-1}^k+C_{n-1}^{k-1})

例题1

划分数
思路

(dp[i][j])(j) 个物品 划分 (i) 次的方案数

可得到: (dp[i][j]=dp[i][j-i]+dp[i-1][j])

意思是将 (j) 个物品分成 (i) 份,有两种情况: (dp[i][j-i]) 每份划分都大于等于 (1) ; (dp[i-1][j]) 存在有一份以上用 (0) 划分

代码
#include<iostream>
#include<cstdio>

using namespace std;
int n,m;
int dp[1200][1200];

int main(){
    scanf("%d%d",&n,&m);
    dp[0][0]=1;
    for(int i=1;i<=m;i++)
        for(int j=0;j<=n;j++){
            if(j>=i) dp[i][j]=dp[i][j-i]+dp[i-1][j];
            else dp[i][j]=dp[i-1][j];
        }
    printf("%d",dp[m][n]);
    return 0;
}

(其实也可以直接拿组合数做, (C_{m-n+1}^n) 就出来了,讲DP是为了思维)

例题2

CF559C Gerald and Giant Chess
思路

​ 先考虑如果没有黑色格子的时候,我们的方案数是 (C_{h+w-2}^{h-1}) ,若再求出左上到右下经过至少一个黑色格子的路线数量,相减就得到了只经过白色格子的路线数。

​ 然后考虑具体做法:把所有黑色格子按照行,列坐标递增的顺排序。并且把左上角和右下角看作黑色格子。设第 (i) 个格子在第 (x_i) 行第 (y_i) 列。设 (F[i]) 表示从左上角走到第 (i) 个黑色格子,途中不经过其他黑色格子的路线数。

[F[i]=C_{x_i-1+y_i-1}^{x_i-1}-sumlimits_{j=0}^{i-1}f[j]*C_{x_i-x_j+y_i-y_j}^{x_i-x_j}~~(x_igeq x_j,y_igeq y_j) ]

这就是状态转移方程了。

代码
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define int long long

using namespace std;
const int N=3e5+50,mod=1e9+7;
int h,w,n,p[N],inv[N],f[N];
struct node{
    int x,y;
}a[N];
bool cmp(node a,node b){
    return a.x<b.x||(a.x==b.x&&a.y<b.y);
}

int C(int m,int n){
    if(m>n) return 0;
    return (p[n]*inv[n-m]%mod*inv[m])%mod;
}

inline int fpow(int a,int b){
    int res=1;
    while(b){
        if(b&1) res=(res*a)%mod;
        b>>=1;
        a=(a*a)%mod;
    }
    return res%mod;
}

signed main(){
    scanf("%lld%lld%lld",&h,&w,&n);
    for(int i=1;i<=n;i++)
        scanf("%lld%lld",&a[i].x,&a[i].y);
        a[n+1].x=h,a[n+1].y=w;
        sort(a+1,a+n+2,cmp);
        for(int i=0;i<=N-10;i++) p[i]=(i==0)?1:((p[i-1]*i)%mod),inv[i]=fpow(p[i],mod-2);
        for(int i=1;i<=n+1;i++)
            f[i]=C(a[i].x-1,a[i].x+a[i].y-2);
            for(int i=1;i<=n+1;i++)
                for(int j=1;j<=i-1;j++){
                    if(a[i].x<a[j].x||a[i].y<a[j].y) continue;
                    f[i]-=f[j]*C(a[i].x-a[j].x,a[i].x+a[i].y-a[j].x-a[j].y);
                    f[i]=(f[i]+mod)%mod;
                }
                printf("%lld
",(f[n+1]+mod)%mod);
                return 0;
}

例题3

POJ 1737 Connected Graph
思路

​ 我们可以考虑求出N个点的无向图总数,减去N个点的不连通无向图数量,就是答案了。

​ N个点的无向图总数是 (2^{N*(N-1)/2}) 。含义是N个点的无向图最多有 (N*(n-1)/2) 条边,边有两种状态:有或无。

​ 接下来计算N个点的不连通数量。我们可以枚举标号为1的节点所在的联通块包含的节点数k,从2~N这N-1个节点中选出k-1个。显然有 (C_{N-1}^{~k-1}) 种选法。剩余N-k个节点构成任意无向图,有 (2^{(N-k)*(N-k-1)/2}) 种方法。

​ 设 (F[i]) 表示i个节点的无向连通图个数,则状态转移方程是:

[F[i]=2^{i*(i-1)/2}-sumlimits_ {j=1}^{i-1}f[j]*C_{i-1}^{j-1}*2^{(i-j)*(i-j-1)/2} ]

注意:因为没有模数,所以需要高精度!!!

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define re register
#define ll long long

using namespace std;
struct Giao{
    int num[1001];
    Giao(){num[0]=1;}
    inline void clear(){
        memset(num,0,sizeof(num)),num[0]=1;
    }
    inline void read(){
        string s;cin>>s,num[0]=s.size();
        for(re int i=1;i<=num[0];i++) num[i]=s[num[0]-i];
    }
    inline void print(){
        for(re int i=num[0];i>0;i--) putchar(num[i]+48);
    }
    inline void operator = (string s){
        num[0]=s.size();
        for(re int i=1;i<=s.size();i++)
            num[i]=num[s.size()-i];
    }
    inline Giao operator + (Giao x){
        Giao y;y.clear();re int i;
        for(i=1;i<=num[0]||i<=x.num[0];i++){
            y.num[i]+=x.num[i]+num[i];
            if(y.num[i]>9) ++y.num[i+1],y.num[i]-=10;
        }y.num[0]=i;
        while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
        return y;
    }
    inline Giao operator - (Giao x){
        Giao y;y.clear();re int i;
        for(i=1;i<=num[0];i++){
            y.num[i]+=num[i]-x.num[i];
            if(y.num[i]<0) --y.num[i+1],y.num[i]+=10;
        }y.num[0]=i;
        while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
        return y;
    }
    inline Giao operator * (Giao x){
        Giao y;y.clear();re int i,j,k;
        for(i=1;i<=num[0];i++){
            k=0;
            for(j=1;j<=x.num[0];j++)
                y.num[i+j-1]+=num[i]*x.num[j]+k,
                    k=y.num[i+j-1]/10,y.num[i+j-1]%=10;
            y.num[i+x.num[0]]+=k;
        }y.num[0]=i+j;
        while(!y.num[y.num[0]]&&y.num[0]>1) --y.num[0];
        return y;
    }
}p2[2501],c[51][51],dp[51];//p2是2的次方 c是组合数 dp就是动态转移方程
int n;

int main(){
    p2[0]="1";
    for(int i=1;i<=2500;i++) p2[i]=p2[i-1]+p2[i-1];
    for(int i=0;i<=50;i++){
        c[i][0]="1";
        for(int j=1;j<=i;j++)
            c[i][j]=c[i-1][j]+c[i-1][j-1];
    }
    while(scanf("%d",&n),n){
        memset(dp,0,sizeof(dp)),dp[1]="1";
        for(int i=2;i<=n;i++){
            dp[i]=p2[i*(i-1)>>1];
            for(int j=1;j<i;j++)
                dp[i]=dp[i]-dp[j]*c[i-1][j-1]*p2[(i-j)*(i-j-1)>>1];
        }dp[n].print(),putchar('
');
    }
    return 0;
}

例题4

POJ 1037 A decorative fence
思路

[F[i,j,0]=sumlimits_{p=j}^{i-1}F[i-1,p,1]\F[i,j,1]=sumlimits_{p=1}^{j-1}F[i-1,p,0] ]

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long

using namespace std;
int t,n;
ll m,f[21][21][2];

inline void init(){
    f[1][1][0]=f[1][1][1]=1;
    for(int i=2;i<=20;i++)
        for(int j=1;j<=i;j++){
            for(int p=j;p<=i-1;p++) f[i][j][0]+=f[i-1][p][1];
            for(int p=1;p<=j-1;p++) f[i][j][1]+=f[i-1][p][0];
        }
}

int main(){
    init();
    scanf("%d",&t);
    while(t--){
        scanf("%d%lld",&n,&m);
        bool used[21];
        memset(used,0,sizeof(used));
        int last,k;
        //第1块木板,即可能处于高位,也可能处于高位,也可能处于低位,单独处理
        for(int j=1;j<=n;j++){
            if(f[n][j][1]>=m){
                last=j,k=1;
                break;
            }
            else m-=f[n][j][1];
            if(f[n][j][0]>=m){
                last=j,k=0;
                break;
            }
            else m-=f[n][j][0];
        }
        used[last]=1;
        printf("%d",last);
        //第2~n块木板,高低位置、合法的长度范围与上一块木板有关
        for(int i=2;i<=n;i++){
            k^=1;
            //真实长度为len,在剩余木板中排名为j
            int j=0;
            for(int len=1;len<=n;len++){
                if(used[len]) continue;
                j++;
                if(k==0&&len<last||k==1&&len>last){
                    if(f[n-i+1][j][k]>=m){
                        last=len;
                        break;
                    }
                    else m-=f[n-i+1][j][k];
                }
            }
            used[last]=1;
            printf(" %d",last);
        }
        puts("");
    }
    return 0;
}