计数类DP
900.整数划分
- 根据题意,将其归于
完全背包问题
,即从1~i
中选择任意多的数,其总和为j
的所有选法- 根据最后一步选择多少
i
求出状态计算递推公式为f[i,j]=f[i-1,j]+f[i,j-i]
- 根据滚动数组优化为
f[j]=f[j]+f[j-i]
//二维写法
#include<iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];
int main(){
int n;
cin>>n;
f[0][0]=1; //从前0个数选择总和为0,即什么都不选,应当算一种方案
for(int i=1;i<=n;i++)
for(int j=0;j<=n;j++){ //选择总和为0也应当计入方案内
f[i][j]=f[i-1][j]; //一个i都不选
if(j>=i)f[i][j]=(f[i][j]+f[i][j-i])%mod; //至少j要能容纳一个i
}
cout<<f[n][n];
}
//一维优化
#include<iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N];
int main(){
int n;
cin>>n;
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
f[j]=(f[j]+f[j-i])%mod;
cout<<f[n];
}
思路二
- 状态转移方程为
f[i,j]=f[i-1,j-1]+f[i-j,j]
- 最后答案枚举从
f[n,1]+...f[n,n]
的和
#include<iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];
int main(){
int n;
cin>>n;
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++){
f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
}
int res=0;
for(int i=1;i<=n;i++)res=(res+f[n][i])%mod;
cout<<res;
}
数位统计DP
338.计数问题
- 我们实现一个函数
count(n,x)
,用于计算1~n 所有数字中,x(0~9)的出现次数
- 例:求1在
1<=xxx1yyy<=abcdefg
这个区间内第四位出现的次数,分情况讨论- 1.
xxx∈[0,abc-1]
此时xxx1yyy一定属于该区间,则yyy=000~999,总共有abc*1000种情况
- 2.
xxx=abc
d<1
(d=0),此时abc1yyy一定大于abc0efg,0
种情况d=1
,此时yyy可取000~efg,总共efg+1
种情况d>1
,此时yyy可取000~999,总共1000
种情况
- 那么求解1在a~b之间出现的次数即可用
count(b,1)-count(a-1,1)
表示
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int gets(vector<int> nums,int l,int r){ //用于统计低位l到高位r之间的数字大小
int res=0;
for(int i=r;i>=l;i--){
res=res*10+nums[i];
}
return res;
}
int pow10(int x){ //用于计算10的x次方
int res=1;
while(x--)res*=10;
return res;
}
int count(int n,int x){ //用于计算1~n 所有数字中,x的出现次数
if(!n)return 0; //n=0不合法,返回0
vector<int> nums; //nums用于按位存储数字n
while(n){
nums.push_back(n%10);
n/=10;
}
n=nums.size();
int res=0;
for(int i=n-1-!x;i>=0;i--){ //枚举x可能出现的位置,从最高位开始枚举,如果x是0,则不可能出现在最高位,从次高位开始枚举
if(i<n-1){ //x不在最高位的情况
res+=gets(nums,i+1,n-1)*pow10(i);
if(!x)res-=pow10(i); //如果x=0,则高位应当是001~abc,故减掉一个10^i
}
if(nums[i]==x)res+=gets(nums,0,i-1)+1; //枚举接下来两种情况
else if(nums[i]>x)res+=pow10(i);
}
return res;
}
int main(){
int a,b;
while(cin>>a>>b , a||b){
if(a>b)swap(a,b);
for(int i=0;i<10;i++){
cout<<count(b,i)-count(a-1,i)<<" ";
}
cout<<endl;
}
}
状态压缩DP
291.蒙德里安的梦想
- 解题思想:假设所有横置的小方块都已经放好,那么列置的小方块只要按顺序依次填空即可(空必须可以填满),那么只要枚举所有
横置的小方块成立的情况
即可- 横置的小方块都是
1行*2列
的,所以横着填一次会填满两格,我们观察第i
列的所有情况,用f[i,j]
表示第i列
中,有j行
被上一列填入的小方格占据(j是二进制,假设j最大是5行,那么被占据第五行就是00001,被占据第4,5行就是00011...以此类推
)- 考虑到
i-1列
同样被i-2列
伸出的小方格占据,即我们考虑f[i-1,k]
和f[i,j]
的状态是否会在第i
列产生冲突,如果k&j==0
即不会在第i
列产生冲突- 同样我们考虑横格放好之后,竖格是否可以正确插入的情况,由于竖方块是
2行*1列
的,假如出现了竖行奇数空行,如3*1
的空格,那么则无法正确插入,即出现连续的奇数空格(st[j|k]
),是不符合条件的答案(j|k如j=10010,k=00001,j|k=10011,那么第k列存在两个空位
)- 我们用
st
数组存放所有竖格不满足条件的情况,例如一共有五行,那么用二进制表示的00011
即是一种不满足条件的情况,而00011对应10进制是3
,所以st[3]
应当存储false
,满足条件的存储true
#include<iostream>
#include<cstring>
using namespace std;
const int N=12,M=1<<N;
int st[M];
long long f[N][M]; //N是列数,M对应行的最大二进制数
int main(){
int n,m; //n行m列
while(cin>>n>>m && n||m){
memset(f,0,sizeof f);
//初始化st数组
for(int i=0;i<1<<n;i++){ //枚举每一个二进制数
st[i]=true; //默认i列是可行方案
int cnt=0; //计数连续的0
for(int k=0;k<n;k++){
if(i>>k&1){ //i当前最后一位是否为1,如果是1,说明连续的0中断
if(cnt&1)st[i]=false; //如果cnt是奇数,那么当前列的方案不可行
cnt=0;
}
else cnt++;
}
if(cnt&1)st[i]=false; //判断最后一组连续的0是否为奇数
}
//dp过程,枚举0~m列状态转移过程
f[0][0]=1; //第0列没有上一列伸出的方块,所以上一列转移来的只有一种情况
for(int i=1;i<=m;i++)
for(int j=0;j<1<<n;j++)
for(int k=0;k<1<<n;k++)
if( (k&j)== 0 && st[k|j])f[i][j]+=f[i-1][k]; //如果可以正确转移,加上前一列所有的方案数
cout<<f[m][0]<<endl; //当m+1列是00000若干个0时(没有方块伸出),是合法答案,输出结果
}
}
最短Hamilton路径
i
是n位二进制,其中0代表没有经过该点,1表示经过了该点,例如i=100001表示
经过第0和第5个点- 状态计算中以
倒数第二个点
经过哪一个点来划分状态,若i-(1<<j)>>k&1==1
即k与j不重合,并且i的k位为1(表示经过k点),则更新状态f[i][j]=f[i-(1<<j)][k]+w[k][j]
,并在若干k中取最小值- 此题注意
'-'号优先度高于'<<'和'>>'
#include<iostream>
#include<cstring>
using namespace std;
const int N=20,M=1<<N;
int f[M][N]; //状态表示
int w[N][N]; //带权图
int main(){
int n;
cin>>n;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
cin>>w[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=0; //从0走到0并且经过第0个点的路径为长度为0
for(int i=0; i<1<<n; i++)
for(int j=0; j<n; j++)
if(i>>j&1){ //枚举的i应当包含终点j
for(int k=0;k<n;k++)
if(i-(1<<j)>>k&1) //枚举的i应当包含倒数第二个点k并且不能与j重合
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
}
cout<<f[(1<<n)-1][n-1]; //输出经过所有点且终点是n-1的路径大小
}
树形DP
285.没有上司的舞会
#include<iostream>
#include<cstring>
using namespace std;
const int N=6010;
int n;
int f[N][2];
int happy[N]; //存放快乐指数
int h[N],e[N],ne[N],idx;
bool has_father[N]; //判断谁是根节点,根节点没有父节点
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u){
f[u][1]=happy[u]; //选择u的情况初值是happy[u]
for(int i=h[u];i!=-1;i=ne[i]){ //dfs所有儿子节点
int j=e[i];
dfs(j);
f[u][1]+=f[j][0]; //分别处理选不选u的两种情况
f[u][0]+=max(f[j][0],f[j][1]);
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>happy[i];
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b;
cin>>a>>b;
add(b,a); //b是a的父节点
has_father[a]=true; //表示a存在父节点
}
int root=1;
while(has_father[root])root++; //寻找根节点
dfs(root);
cout<<f[root][0]<<" "<<f[root][1]<<endl;
cout<<max(f[root][0],f[root][1]);
return 0;
}
记忆化搜索
901.滑雪
- 使用记忆化数组 记录每个点的最大滑动长度
遍历每个点作为起点
然后检测该点四周的点 如果可以滑动到其他的点
那么该点的最大滑动长度 就是其他可以滑到的点的滑动长度+1dp[i][j] = max(dp[i][j-1], dp[i][j+1],dp[i-1][j],dp[i+1][j])
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=310;
int map[N][N];
int r,c;
int f[N][N];
int dx[4]={1,0,-1,0} ,dy[4]={0,1,0,-1};
int dp(int i,int j){
int &v=f[i][j]; //v代替f[i][j]
if(v!=-1)return v; //记忆化的好处
v=1; //初始路径为1
for(int k=0;k<4;k++){
int a=i+dx[k],b=j+dy[k];
if(a>=0 && b>=0 && a<r && b<c && map[a][b]<map[i][j])v=max(v,dp(a,b)+1);
}
return v;
}
int main(){
cin>>r>>c;
for(int i=0;i<r;i++)
for(int j=0;j<c;j++)
cin>>map[i][j];
memset(f,-1,sizeof f); //没有枚举过的点初值为-1
int res=0;
for(int i=0;i<r;i++)
for(int j=0;j<c;j++)
res=max(res,dp(i,j)); //dp每一个点,找寻最大值
cout<<res;
}