计数 DP 学习笔记
前言
只是对遇到的计数类 dp 题目的记录,遇到了其他的难题也不一定会做。
题目实际上是蓝书的题目。文字也有不少是借鉴的蓝书题解。
简介
计数类 dp 强调不重不漏。因此,在求解计数类 dp 时,通常要找到一个“基准点”,围绕这个基准点构造一个不可划分的整体,避免子问题的重叠。
具体看几道题就知道这段话的意思了。
P10956 金字塔
一道区间 dp 和计数 dp 结合的题目。
不难想到状态:
现在来探究序列的组成与划分。若
所以是否可以将 AB|A 和 A|BA 都能划分出 A|B|A,而此时就出现了重复。
于是我们的计数方法:找基准点就出来了。我们只用考虑字串
那么就有转移:若
初始有
code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=305,Mod=1e9;
int n;
string s;
int dp[N][N];
signed main(){
cin>>s;
n=s.size(),s=" "+s;
for(int i=1;i<=n;i++)
dp[i][i]=1;
for(int l=3;l<=n;l+=2){
for(int i=1;i+l-1<=n;i++){
int j=i+l-1;
if(s[i]!=s[j])
continue;
dp[i][j]=dp[i+1][j-1];
for(int k=i+2;k<=j-2;k++)
if(s[i]==s[k])
dp[i][j]+=dp[i+1][k-1]*dp[k][j],dp[i][j]%=Mod;
}
}
cout<<dp[1][n];
return 0;
}
P10982 连通图
同样考虑将问题划分成两个部分,其中一个为独立的不可划分的整体,作为我们的基准点。
因为连通图不好划分,考虑将问题转化为计算非连通图。那么就有 全集-非连通图个数=连通图个数。对于任何一个有
所以,设
code:
#include<bits/stdc++.h>
#define int long long
#define Mod 1004535809
#define Inv(x,p) Fpow(x,p-2,p)
using namespace std;
const int N=1e3+5;
int n;
int fac[N]={1},dp[N];
int Fpow(int a,int b,int p){
int ans=1;
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p,b>>=1;
}
return ans;
}
int C(int n,int m,int p){
if(n<m)return 0;
return ((fac[n]*Inv(fac[m],p))%p*Inv(fac[n-m],p))%p;
}
signed main(){
cin>>n;
for(int i=1;i<=1000;i++)
fac[i]=fac[i-1]*i%Mod;
dp[1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++)
dp[i]+=dp[j]*C(i-1,j-1,Mod)%Mod*Fpow(2,(i-j)*(i-j-1)/2,Mod)%Mod,dp[i]%=Mod;
dp[i]=Fpow(2,i*(i-1)/2,Mod)-dp[i],dp[i]=((dp[i]%Mod)+Mod)%Mod;
}
cout<<dp[n];
return 0;
}
P7690 [CEOI2002] A decorative fence
这里又要介绍计数 dp 的另一种 trick 了,它就是“试填法”。它主要是解决问排名为
拿这道题举例,我们从小到大枚举每一块木板的长度,当当前这一块木板的长度为
设
试填的过程如上,但是需要注意,
code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=25;
int T,n,m;
bool vis[N];
int dp[N][N][2];
void init(){
dp[1][1][1]=dp[1][1][0]=1;
for(int i=2;i<=20;i++){
for(int j=1;j<=i;j++){
for(int k=1;k<j;k++)
dp[i][j][1]+=dp[i-1][k][0];
for(int k=j;k<i;k++)
dp[i][j][0]+=dp[i-1][k][1];
}
}
return;
}
signed main(){
init();cin>>T;
while(T--){
cin>>n>>m;
int k,pre;
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++){//分类枚举1的状态
if(m>dp[n][i][1])m-=dp[n][i][1];
else{k=0,pre=i;break;}
if(m>dp[n][i][0])m-=dp[n][i][0];
else{k=1,pre=i;break;}
}
vis[pre]=1;cout<<pre<<" ";
for(int i=2;i<=n;i++){//一位一位确定状态
int rk=0;
for(int j=1;j<=n;j++){//rk是长度j的排名
if(vis[j])continue;
rk++;
if((k&&j>pre)||(!k&&j<pre)){//符合要求
if(m>dp[n-i+1][rk][k])m-=dp[n-i+1][rk][k];
else{k^=1,pre=j;break;}
}
}
vis[pre]=1;cout<<pre<<" ";
}
cout<<"\n";
}
return 0;
}