简单计数DP
计数DP
顾名思义,这是对于方案统计的DP类型
需要记住的公式:
- 在平面直角坐标系中,从点
走到 , 每一步只能使得 或者是 ,求合法路径的条数为:
证明:因为每一步只能向右或者向上走,所以我们可以把每一步抽象为一个物品,共有
- 一张含有
个节点的无向图一共有 种可能性(节点不编号) - 一张含有
个节点的连通无向图(节点有编号),一共有的可能性数量为:
初值:
4. 一张含有
设
其中
有公式;
初值:
- 在一个长度为n的环里,可以交换任意两个节点,使得这个环变为n个自环且使用最少步骤的方案有
下面的例题中会详细推导这一整个公式,并附上代码
注意事项:
-
之前提到过的:在DP的方案统计的时候,如果一个状态的多个决策之间满足加法原理,而各个决策的方案又满足乘法原理时,在状态转移的时候各个决策必须具备互斥性才不会重复,对于互斥性的问题,我们可以挖掘题设条件,在DP的维度进行限制,比如改变某个维度的含义或者对这个含义进行缩小范围
-
擅长从正反两面思考问题,如果正着不好处理,不妨采用总-反的方式,这是弱化版的容斥原理
-
合理运用组合计数公式以及容斥原理等,记得有时可以打表找感觉
-
十年OI一场空,不开longlong见祖宗
-
注意方案之间的重复性,一定得做到不重不漏
-
在实现代码设计状态的时候可以使用等效转换,将问题变得更容易统计,一般是在不影响答案的情况下将绝对性表述换为相对性表述
-
注意方案之间的内在联系,方案之间也可以求排名,求排名的时候需要像平衡树那样把小的一坨全删了,具体见装饰围栏
套路
- 思路1:为了不重不漏,在计数DP的设计中,我们会经常使用围绕一个(或者是两个)基准点,构造一个不可划分的整体,对此进行计算(见例1)
- 思路2:构造法,将需要的构造出来,见例2
- 一般需要复杂度
的是数学题,复杂度 是DP题
下面就开题了:
例题:How many of them
题目描述
在无向连通图中,若一条边被删除后,图会分成不连通的两部分,则称该边为割边。
求满足如下条件的无向连通图的数量:
1、由 N 个节点构成,节点有标号,编号为 1∼N。
2、割边不超过 M 条。
3、没有自环和重边。
熟悉不,这就是我们之前提到的第四条公式,下面展开详细推导
首先我们先讨论
在图论中,我们称不含割边的图为双连通分量,很明显,一个含有割边的无向图一定有着双连通分量
所以我们先来考虑当节点1处于一个大小为
而当
,现在我们来讨论
代码贴一份,这题时间紧,这份代码常数较大最后两个点过不了,预处理一下组合数(递推),取消#define int long long
,再预处理一下2的幂,在代码中处理
#include<iostream>
#include<cstdio>
using namespace std;
#define int long long
int f[55][55],g[55][55][55],jc[55],inv[55],h[55],p=1e9+7,n,m;
int power(int a,int b){
int ans=1;
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p;
b>>=1;
}
return ans;
}
void init(){
inv[0]=jc[0]=1;
for(int i=1;i<=n;i++)jc[i]=jc[i-1]*i%p,inv[i]=power(jc[i],p-2)%p;
}
int C(int n,int m){
return jc[n]%p*inv[m]%p*inv[n-m]%p;
}
signed main(){
scanf("%lld%lld",&n,&m);
g[0][0][0]=1;
init();
for(int i=1;i<=n;i++){
h[i]=power(2,i*(i-1)/2);
for(int j=1;j<i;j++){
h[i]=(h[i]-h[j]*C(i-1,j-1)%p*power(2,(i-j)*(i-j-1)/2)%p)%p;
}
f[i][0]=h[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
for(int k=1;k<i;k++){
int s=0;
for(int x=1;x<=min(i-k,j);x++)s=(s+g[i-k][x][j-x]*power(k,x)%p)%p;
f[i][j]+=f[k][0]*C(i-1,k-1)%p*s%p;
}
f[i][0]=((f[i][0]-f[i][j])%p+p)%p;
}
for(int j=1;j<=i;j++)
for(int k=0;k<i;k++)
for(int p1=1;p1<=i;p1++)
for(int q=0;q<=k;q++)
g[i][j][k]=(g[i][j][k]+f[p1][q]*C(i-1,p1-1)%p*p1%p*g[i-p1][j-1][k-q])%p;
}
int ans=0;
for(int i=0;i<=m;i++)ans=(ans+f[n][i])%p;
printf("%lld",(ans%p+p)%p);
}
例题:扑克牌
题面描述:
一副不含王的扑克牌由 52 张牌组成,由红桃、黑桃、梅花、方块 4 组牌组成,每组 13 张不同的面值。
现在给定 52 张牌中的若干张,请计算将它们排成一列,相邻的牌面值不同的方案数。
牌的表示方法为 XY,其中 X 为面值,为 2、3、4、5、6、7、8、9、T、J、Q、K、A 中的一个。
Y 为花色,为 S、H、D、C 中的一个。
如 2S、2H、TD 等。
有T组数据,每组数据有N张牌,时间限制1s
分析:在此题中,我们设
我们发现,
- 题目上的限制仅仅是两个不同而已,于是乎我们并不关心两者的具体权值,只关心是不是相等
- 每种牌的数量很少,这启发我们构建一个以牌的数量为状态的状态转移方程
- 同类牌是可以互相交换的
这题的难度就在状态的设计
因为未使用的牌的数量只有可能为
设
ull ask(int a,int b,int c,int d,int e){
ull& res=f[a][b][c][d][e];
if(res)return res;
if(a)res+=ask(a-1,b,c,d,1)*(a-(e==2));
if(b)res+=ask(a+1,b-1,c,d,2)*2*(b-(e==3));
if(c)res+=ask(a,b+1,c-1,d,3)*3*(c-(e==4));
if(d)res+=ask(a,b,c+1,d-1,4)*4*d;
return res;
}
我们以第一部分为例讲解状态转移方程是怎么来的
由于我们是按照记忆化搜索的形式实现的自顶向下的动态规划,于是我们令上一次的状态为
对于我们第一个方程,当
我们会发现,之所以我们的判断条件是
由于状态转移方程比较复杂,循环形式难以实现,故采用递归实现
#include<iostream>
#define ull unsigned long long
using namespace std;
int n,t,val[75],cnt[5],num[55],tot;
ull f[15][15][15][15][5];
char s[1005];
ull ask(int a,int b,int c,int d,int e){
ull& res=f[a][b][c][d][e];
if(res)return res;
if(a)res+=ask(a-1,b,c,d,1)*(a-(e==2));
if(b)res+=ask(a+1,b-1,c,d,2)*2*(b-(e==3));
if(c)res+=ask(a,b+1,c-1,d,3)*3*(c-(e==4));
if(d)res+=ask(a,b,c+1,d-1,4)*4*d;
return res;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
for(int i=0;i<=4;++i)f[0][0][0][0][i]=1;
int res=0;
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i)cin>>s,++val[s[0]-49];
for(int i=1;i<=4;++i)cnt[i]=0;
for(int i=0;i<=50;++i)++cnt[val[i]],val[i]=0;
cout<<"Case #"<<++res<<": "<<ask(cnt[1],cnt[2],cnt[3],cnt[4],0)<<"\n";
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!