计数dp小结

序:除了刚开始的看了几道题的题解,后来也自己肛出了几道 剩下不可做的题不也没做吗
这些题目最大的特点是在于需要自己构造状态,这往往会成为一道题的最大卡点 穷举表示水不到几分


题目选讲:
E:
如果直接模拟,复杂度为 kn2
既然每一步只能往上下或往左右走,
那么我们可以把题目分解为在x轴上行走k步与在y轴上行走k步的方案数都处理出来 kn
然后枚举往左右走i步,往上下走k-i步,然后再用组合处理(常规套路)

#include<cstdio>
#include<cstring>
#define P 9999991
int dp[2][1005][1005];
long long Sum[2][1005];
int C[1005][1005];
int main(){
    int T;
    scanf("%d",&T);
    for(int i=1;i<=1000;i++){
        C[i][0]=C[i][i]=1;
        for(int j=1;j<i;j++){
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
        }
    }
    for(int cas=1;cas<=T;cas++){
        int n,m,k,x0,y0;
        memset(dp,0,sizeof(dp));
        memset(Sum,0,sizeof(Sum));
        scanf("%d%d%d%d%d",&n,&m,&k,&x0,&y0);
        dp[0][0][x0]=1;
        dp[1][0][y0]=1;
        Sum[1][0]=1;
        Sum[0][0]=1;
        for(int j=1;j<=k;j++){
            for(int i=1;i<=n;i++){
                if(i>1)dp[0][j][i]+=dp[0][j-1][i-1];
                if(i>2)dp[0][j][i]+=dp[0][j-1][i-2];
                if(i<n)dp[0][j][i]+=dp[0][j-1][i+1];
                if(i+1<n)dp[0][j][i]+=dp[0][j-1][i+2];
                dp[0][j][i]%=P;
                Sum[0][j]+=dp[0][j][i];
                Sum[0][j]%=P;
            }
        }
        for(int j=1;j<=k;j++){
            for(int i=1;i<=m;i++){
                if(i>1)dp[1][j][i]+=dp[1][j-1][i-1];
                if(i>2)dp[1][j][i]+=dp[1][j-1][i-2];
                if(i<m)dp[1][j][i]+=dp[1][j-1][i+1];
                if(i+1<m)dp[1][j][i]+=dp[1][j-1][i+2];
                dp[1][j][i]%=P;
                Sum[1][j]+=dp[1][j][i];
                Sum[1][j]%=P;
            }
        }
        long long ans=0;
        for(int i=0;i<=k;i++){
            ans+=Sum[0][i]*Sum[1][k-i]%P*C[k][i]%P;
            ans%=P;
        }
        printf("Case #%d:\n%lld\n",cas,ans);
    }
    return 0;
}

F:
又是一道 关于位运算的题目,一看到数据范围,就可以揣测出大概复杂度
求两个序列,正着扫一遍,倒着扫一遍。然后枚举两点(左序列的右端点,右序列的左端点),进行运算,这样复杂度为 O(n3) 加一个前缀和优化就是 O(n2)

#include<cstdio>
#include<cstring>
#define P 1000000007 
const int M=1023;
int A[1005];
long long dp[2][1005][M+5],dp1[2][1005][M+5];
long long Sum[1005][M+5];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        memset(dp,0,sizeof(dp));
        memset(dp1,0,sizeof(dp1));
        memset(Sum,0,sizeof(Sum));
        int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)scanf("%d",&A[i]);
        dp[0][0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j<=M;j++){
                dp[0][i][j]+=dp[1][i-1][j]+dp[0][i-1][j];
                dp[0][i][j]%=P;
            }
            for(int j=0;j<=M;j++){
                int t=A[i]^j;
                dp[1][i][t]+=dp[0][i-1][j]+dp[1][i-1][j];
                dp[1][i][t]%=P;
            }
        }
        dp1[0][n+1][M]=1;
        for(int i=n;i>=1;i--){
            for(int j=0;j<=M;j++){
                dp1[0][i][j]+=dp1[1][i+1][j]+dp1[0][i+1][j];
                dp1[0][i][j]%=P;
            }
            for(int j=0;j<=M;j++){
                int t=j&A[i];
                dp1[1][i][t]+=dp1[0][i+1][j]+dp1[1][i+1][j];
                dp1[1][i][t]%=P;
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=0;j<=M;j++){
                Sum[i][j]=Sum[i-1][j]+dp[1][i][j];
                Sum[i][j]%=P;
            }
        }
        long long ans=0;
        for(int j=0;j<=M;j++){
            for(int i=1;i<n;i++){
                ans+=Sum[i][j]*dp1[1][i+1][j]%P;
                ans%=P;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

G:
像这种两维的题目可以存一维,枚举一维
定义dp两维,一维是枚举到哪一列,另一维是有几行是空着的
对于不同的情况,进行填充空行,或者是填充填过的行

#include<bits/stdc++.h>
using namespace std;
const int M=55,P=1000000007;
long long dp[55][55],C[M][M],Pow[M];
void init(){
    for(int i=1;i<=50;i++){
        C[i][0]=C[i][i]=1;
        for(int j=1;j<i;j++){
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
        }
    }
    Pow[0]=1;
    for(int i=1;i<=50;i++)Pow[i]=Pow[i-1]*2%P;
}
int main(){
    init();
    int n,m;
    while(~scanf("%d %d",&n,&m)){
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=m;i++)dp[1][i]=1;
        for(int i=1;i<=n;i++)dp[i][1]=1;
        for(int i=2;i<=n;i++){
            for(int j=2;j<=m;j++){
                dp[i][j]+=1ll*dp[i][j-1]*(Pow[i]-1)%P;
                dp[i][j]%=P;
                for(int k=1;k<i;k++){
                    dp[i][j]+=dp[i-k][j-1]*C[i][k]%P*Pow[i-k]%P;
                    dp[i][j]%=P;
                }
            }
        }
        printf("%lld\n",dp[n][m]);
    }
    return 0;
}

H:
这道题的难度相比于上面的题目略大
在树形dp的套路上还要加上组合数
虽然说是1-n的序列,但我们只用处理他们之间的大小关系就好了
可以证明:任意大小关系都可被不同的序列表示
在合并两个子树的时候,我们已知子树自身节点的大小关系,但并不知道两棵树之间的大小关系,且这个关系是确定的,那么就可以乘上 C(size(A)size(A)+size(B))
合并完子树之后,再加入根节点,根节点作为最大值时 dp[i]+=dp[i1]
不然就是 dp[i]+=dp[i]size(A) size(A) 为以某节点根的树的的节点个数

#include<cstdio>
#include<vector>
#include<cstring> 
using namespace std;
#define P 1000000007
vector<int>edge[1005];
long long dp[1005][1005];//i节点,j个最大值 
long long dp1[1005][1005];
int C[1005][1005];
int degree[1005],n;
void dfs(int x,int f){
    bool flag=0;
    int cnt=0;
    for(int k=0;k<(int)edge[x].size();k++){
        int y=edge[x][k];
        if(y==f)continue;
        dfs(y,x);
        if(!flag)for(int i=1;i<=n;i++)dp[x][i]=dp[y][i];
        else {
            for(int i=1;i<=n;i++){
                if(!dp[x][i])continue;
                for(int j=1;j<=n;j++){
                    if(!dp[y][j])continue;
                    dp1[x][i+j]+=dp[x][i]*dp[y][j]%P*C[cnt+degree[y]][degree[y]]%P;
                    dp1[x][i+j]%=P;
                }
            }
            for(int i=1;i<=n;i++){
                dp[x][i]=dp1[x][i];
                dp1[x][i]=0;
            }
        }
        cnt+=degree[y];
        flag=1;
    }
    degree[x]=cnt+1;
    if(!flag)dp[x][1]=1;
    else {
        for(int i=1;i<=n;i++)dp1[x][i]=dp[x][i];
        for(int i=1;i<=n;i++)dp[x][i]=dp[x][i]*(degree[x]-1)%P;
        for(int i=1;i<=n;i++){dp[x][i+1]+=dp1[x][i];dp1[x][i]=0;dp[x][i+1]%=P;}
    }
}
void Init(){
    for(int i=0;i<=1000;i++){
        C[i][0]=C[i][i]=1;
        for(int j=1;j<i;j++){
            if(i)C[i][j]=(C[i-1][j-1]+C[i-1][j])%P;     
        }
    }
}
int main(){
    int T,t=0;
    scanf("%d",&T);
    Init(); 
    while(T--){
        int k;
        scanf("%d%d",&n,&k);

        for(int i=1;i<=n;i++)edge[i].clear();
        memset(dp,0,sizeof(dp));
        memset(dp1,0,sizeof(dp1));

        for(int i=1;i<n;i++){
            int x,y;
            scanf("%d%d",&x,&y);
            edge[x].push_back(y);
            edge[y].push_back(x);
        }
        dfs(1,0);
        printf("Case #%d: %lld\n",++t,dp[1][k]);
    }
    return 0;
}

K: 若直接根据题意模拟取了几个数,然后用组合数来计算,复杂度为 n3
然后思考优化,对于满足条件的一组数,保证至少取两个数,且不去两个数就好了,那么dp只用存 选到哪个数 累和为多少 不选几个数 选几个数
由于不选2个数和不选3.4….n个数的效果都是一样的,所以只用存0-2就好了
同理与选几个数

#include<cstdio>
#include<cstring>
#define P 1000000007
int dp[1005][1005][3][3];//选到那个数  得到的值  可以不选   必须选 
int A[1005];
void Add(int &x,int y){
    x+=y;
    if(x>=P)x-=P;
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        memset(dp,0,sizeof(dp));
        int n,s;
        scanf("%d%d",&n,&s);
        for(int i=1;i<=n;i++)scanf("%d",&A[i]);
        dp[0][0][0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j<=s;j++){
                for(int k=0;k<=2;k++){
                    Add(dp[i][j][0][k],dp[i-1][j][0][k]);
                    Add(dp[i][j][1][k],dp[i-1][j][0][k]);
                    Add(dp[i][j][1][k],dp[i-1][j][1][k]);
                    Add(dp[i][j][2][k],dp[i-1][j][1][k]);
                    Add(dp[i][j][2][k],dp[i-1][j][2][k]);
                }
                if(j+A[i]<=s){
                    for(int k=0;k<=2;k++){
                        Add(dp[i][j+A[i]][k][0],dp[i-1][j][k][0]);
                        Add(dp[i][j+A[i]][k][1],dp[i-1][j][k][1]);
                        Add(dp[i][j+A[i]][k][1],dp[i-1][j][k][0]);
                        Add(dp[i][j+A[i]][k][2],dp[i-1][j][k][1]);
                        Add(dp[i][j+A[i]][k][2],dp[i-1][j][k][2]);
                    }
                }
            }
        }
        int ans=0;
        for(int i=1;i<=s;i++)Add(ans,dp[n][i][2][2]);
        printf("%lld\n",1ll*ans*4%P);
    }
    return 0;
}

M:
是一道裸的递推题
可以预处理出用j种颜色染i个格子的方案数(第二类斯特林数)
然后枚举第一行用几种颜色,第二行用几种颜色
同时对于染色的方案再乘上颜色数量的全排
然后就可以在 O(n2) 的时间内算出答案

#include<cstdio>
#define M 2005
#define P 1000000007
long long dp[M][M],C[M][M],A[M];
void Init(){
    dp[0][0]=1;
    for(int i=1;i<=2000;i++){
        for(int j=1;j<=2000;j++){
            dp[i][j]=dp[i-1][j]*j+dp[i-1][j-1];
            dp[i][j]%=P;
        }
    }
    for(int i=1;i<=2000;i++){
        C[i][0]=C[i][i]=1;
        for(int j=1;j<i;j++){
            C[i][j]=C[i-1][j]+C[i-1][j-1];
            C[i][j]%=P;
        }
    }
    A[0]=1;
    for(int i=1;i<=2000;i++){
        A[i]=A[i-1]*i%P;
    }
}
int main(){
    Init();
    int T;
    scanf("%d",&T);
    while(T--){
        int n,m;
        long long ans=0;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++){
            for(int j=1;i+j<=m;j++){
                ans+=dp[n][i]*A[i]%P*C[m][i]%P*dp[n][j]%P*A[j]%P*C[m-i][j]%P;
                ans%=P;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值