目录
计数类dp
给定方案限制,统计某一种方案出现个数
原题链接
https://www.acwing.com/problem/content/902/
题目大意
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n的一种划分。现在给定一个正整数 n
,请你求出 n 共有多少种不同的划分方法。
0<n<=1000
思路
通过完全背包解决。
f ( i , j ) f(i,j) f(i,j)表示前 i i i个数放入容量为 j j j的背包的组合数量。由于使用 [ 1 − ( j − 1 ) ] [1-(j-1)] [1−(j−1)]凑 j j j,所以一定可以凑满。
朴素版:
状态表示: f ( i , j ) f(i,j) f(i,j)表示用1到i的数凑j的数量
状态计算: f ( i , j ) = f ( i − 1 , j ) + f ( i − 1 , j − 1 ∗ i ) + f ( i − 1 , j − 2 ∗ i ) + . . . f(i,j)=f(i-1,j)+f(i-1,j-1*i)+f(i-1,j-2*i)+... f(i,j)=f(i−1,j)+f(i−1,j−1∗i)+f(i−1,j−2∗i)+...
优化版:
状态表示:同朴素版,但是用到了滚动数组优化
状态计算: f ( j ) = f ( j ) + f ( j − i ) f(j)=f(j)+f(j-i) f(j)=f(j)+f(j−i)
具体的证明看这篇博客。
https://blog.csdn.net/qq_45931661/article/details/119999547
其他版
状态表示: f ( i , j ) f(i,j) f(i,j)表示用 j j j个数凑 i i i的数量
状态计: f ( i , j ) = f ( i − 1 , j − 1 ) + f ( i − j , j ) f(i,j)=f(i-1,j-1)+f(i-j,j) f(i,j)=f(i−1,j−1)+f(i−j,j)
代码
朴素版,O(n^3)的复杂度
#include<iostream>
using namespace std;
const int N = 1005,mod = 1e9+7;
int n;
int f[N][N];
int main(){
cin>>n;
for(int i=0;i<=n;i++) f[i][0]=1;
for(int i=1;i<=n;i++){
//枚举物品
for(int j=1;j<=n;j++){
//枚举背包容量
for(int k=0;k*i<=j;k++){
//状态计算
f[i][j]=(f[i][j]+f[i-1][j-k*i])%mod;
}
}
}
cout<<f[n][n]<<endl;
return 0;
}
优化版,O(n^2)的复杂度。
#include<iostream>
using namespace std;
const int N = 1005,mod = 1e9+7;
int n;
int f[N];
int main(){
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]<<endl;
return 0;
}
其他版:
#include<iostream>
using namespace std;
const int N = 1005,mod = 1e9+7;
int n;
int f[N][N];
int main(){
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<<endl;
return 0;
}
数位统计DP
统计数字中的一些规律,如出现数字的次数等等。
题目链接:
https://www.acwing.com/problem/content/340/
题目大意:
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0 出现 10次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…
思路:
求[a,b]区间内x出现的次数,可以采用前缀和的思想, r e s = c o u n t ( b , x ) − c o u n t ( a − 1 , x ) res=count(b,x)-count(a-1,x) res=count(b,x)−count(a−1,x)
现在分析如何求解 c o u n t ( a , x ) count(a,x) count(a,x),即[1,a]中x出现的次数。
设要求的数为abcdefg
以分析第4位出现x的情况为例:
- x!=0
1.形式: { 0 − ( a b c − 1 ) } d { 0 − 1000 } \{0 - (abc-1)\}d\{0-1000\} { 0−(abc−1)}d{ 0−1000}
意义:前缀小于abc的情况
2.1.形式: a b c d { 0 − 1000 } abcd\{0-1000\} abcd{ 0−1000}
意义:d==x的情况
2.2 形式: a b c d { 0 − e f g } abcd\{0-efg\} abcd{ 0−efg}
意义:d<x的情况
2.3 形式:不存在
意义:d>x的情况 - x==0
1.形式: { 1 − ( a b c − 1 ) } d { 0 − 1000 } \{1 - (abc-1)\}d\{0-1000\} { 1−(abc−1)}d{ 0−1000}
意义:前缀小于abc的情况,但是要注意0的前缀不能为0,否则就没有意义了。
2.1.形式: a b c d { 0 − 1000 } abcd\{0-1000\} abcd{ 0−1000}
意义:d==x的情况
2.2 形式: a b c d { 0 − e f g } abcd\{0-efg\} abcd{ 0−efg}
意义:d<x的情况
2.3 形式:不存在
意义:d>x的情况
枚举每一位上的x出现的次数,就可以的到最终的答案。
代码:
#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
int n,m;
int get(vector<int > num, int r, int l){
//将num的r到l位化成一个数字
int res=0;
for(int i=r;i>=l;i--){
res=res*10+num[i];
}
return res;
}
int count(int a,int x){
//统计1~a当中x出现的次数
if(a==0) return 0;
vector<int > num;//存储每一位
while(a){
//低下标放低位
num.push_back(a%10);
a/=10;
}
int n = num.size();
long long res=0;
for(int i=n-1-!x;i>=0;i--){
//从高到低枚举每一位
res+=(long long )get(num,n-1,i+1)*pow(10,i);//1情况
if(!x) res-=pow(10,i);
if(x==num[i]) res+=get(num,i-1,0)+1;//2.1情况
else if(x<num[i]) res+=pow(10,i);//2.2情况
}
return res;
}
int main(){
while(cin>>n>>m,n||m){
if(n>m) swap(n,m);
for(int i=0;i<10;i++){
cout<<count(m,i)-count(n-1,i)<<" ";//利用前缀和的思想
}
cout<<endl;
}
return 0;
}
状态压缩DP
通过将状态用二进制位表示来实现dp的过程。
蒙德里安的梦想
原题链接:
https://www.acwing.com/problem/content/293/
题目大意:
求把 N×M 的棋盘分割成若干个 1×2的的长方形,有多少种方案。
例如当 N=2,M=4时,共有 5 种方案。当 N=2,M=3 时,共有 3种方案。
如下图所示:
思路:
1.可以发现,一旦给定横着的积木的位置,竖着的积木的位置也确定了。
2.用j的二进制来表示每一列的状态。如下图,第i列的状态就是 ( 100 ) 2 (100)_2 (100)2
**状态表示: f ( i , j ) f(i,j) f(i,j)表示第 i i i列, 状态为 j j j的填充方案个数。
状态计算: f ( i , j ) + = f ( i − 1 , k ) f(i,j)+=f(i-1,k) f(i,j)+=f(i−1,k),但是这个递推式有两个要求。**
1. ( j & k ) = = 0 ( j\& k)==0 (j&k)==0
因为假设是在k的二进制1位往左填充一个横向积木。如果j和k有一位同时为1,则会产生冲突。
2. ( j ∣ k ) (j|k) (j∣k)不存在连续的奇数个0
如果存在连续奇数个0,而0表示不用横着的积木。则这个位置只用竖着的积木进行填充,而竖着的积木无法完成奇数高度的填充。所以会留下空缺位,这里进行舍去。
代码
不要像我一样把左移运算写成右移运算了。。。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1<<12;
int n,m;
long long f[15][N];
bool st[N];
int main(){
while(cin>>n>>m, n||m){
memset(f,0,sizeof f);
//预处理
for(int i = 0; i < 1<<n; i++){
//i代表状态
int cnt = 0;//存储连续0个数
st[i] = true;
for(int j = 0; j < n;j++){
//枚举状态的每一位,判断是否有连续奇数个0
if(i>>j&1){
if(cnt&1) st[i] = false;
cnt = 0;
}else cnt++;
}
if(cnt&1) st[i]=false;//判断最后连续的0
}
f[0][0]=1;
for(int i=1;i<=m;i++){
//枚举列
for(int j=0;j<1<<n;j++){
for(int k=0;k<(1<<n);k++){
if((j&k) == 0 && st[j|k]){
f[i][j]+=f[i-1][k];
}
}
}
}
cout<<f[m][0]<<endl;
}
return 0;
}
最短Hamilton路径
原题链接:
https://www.acwing.com/problem/content/93/
题目大意
给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1的最短 Hamilton 路径。
Hamilton 路径的定义是从 0到 n−1 不重不漏地经过每个点恰好一次。
思路
状态表示: f ( i , j ) f(i,j) f(i,j)表示从0出发到j,经过i的情况。i用二进制表示,如1001表示经过0号结点和3号结点的路径。
状态转移: f ( i , j ) = m i n ( f [ t ] [ k ] + w [ k ] [ j ] ) , t 表 示 经 过 i 的 前 一 个 状 态 , k 表 示 当 前 在 t 状 态 的 哪 个 节 点 上 。 f(i,j)=min(f[t][k]+w[k][j]),t表示经过i的前一个状态,k表示当前在t状态的哪个节点上。 f(i,j)=min(f[t][k]+w[k][j]),t表示经过i的前一个状态,k表示当前在t状态的哪个节点上。
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 25;
int f[1<<20][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;
for(int i=1;i<1<<n;i++){
//状态
for(int j=0;j<n;j++){
//结点
if(( i>>j ) & 1){
int t = i-(1<<j);//回溯可能转移到i的状态
for(int k=0;k<n;k++){
if(t>>k & 1){
//确定当前结点
f[i][j] = min(f[t][k]+w[k][j],f[i][j]);
}
}
}
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
树形dp
原题链接:
https://www.acwing.com/problem/content/287/
题目大意
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
思路
状态表示: f ( i , j ) , j ∈ { 0 , 1 } f(i,j),j∈\{0,1\} f(i,j),j∈{
0,1}表示以i为根节点的子树的最大值,j=0表示不取i号结点,j=1表示取i号结点。
状态计算:
f ( i , 0 ) = s u m ( m a x ( f ( k 1 , 0 ) , f ( k 1 , 1 ) ) , m a x ( f ( k 2 , 0 ) , f ( k 2 , 1 ) ) . . . m a x ( f ( k n , 0 ) , f ( k n , 1 ) ) ) , k f(i,0)=sum(max(f(k_1,0),f(k_1,1)),max(f(k_2,0),f(k_2,1))...max(f(k_n,0),f(k_n,1))),k f(i,0)=sum(max(f(k1,0),f(k1,1)),max(f(k2,0),f(k2,1))...max(f(kn,0),f(kn,1))),k表示i的子结点。
f ( i , 1 ) = s u m ( f ( k 1 , 0 ) , f ( k 2 , 0 ) . . . f ( k n , 0 ) ) f(i,1)=sum(f(k_1,0),f(k_2,0)...f(k_n,0)) f(i,1)=sum(f(k1,0),f(k2,0)...f(kn,0))
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 6005;
int f[N][2];
int H[N],has_head[N];
int h[N],e[N],ne[N],ind;
void add(int a,int b){
//把b加入a中
e[ind]=b,ne[ind]=h[a],h[a]=ind++;
}
void dfs(int root){
//dfs进行dp
for(int i=h[root];i!=-1;i=ne[i]){
int t = e[i];
dfs(t);
f[root][0] += max(f[t][0],f[t][1]);
f[root][1] += f[t][0];
}
f[root][1] += H[root];
}
int main(){
memset(h,-1,sizeof h);
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>H[i];
}
for(int i = 0; i < n-1; i++){
int a,b;
cin>>a>>b;
has_head[a] = true;
add(b, a);//构建邻接表
}
int root=1;
while(has_head[root]) root++;//找根节点
dfs(root);
cout<<max(f[root][0],f[root][1])<<endl;
return 0;
}
记忆化搜索
原题链接:
https://www.acwing.com/problem/content/903/
题目大意:
给定一个 R 行 C列的矩阵,表示一个矩形网格滑雪场。矩阵中第 i行第 j 列的点表示滑雪场的第 i 行第 j列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
输出一个整数,表示可完成的最长滑雪长度。
思路:
状态表示: s o l v e ( i , j ) solve(i,j) solve(i,j)表示从 ( i , j ) (i,j) (i,j)出发的最长滑雪长度。
状态计算: s o l v e ( x , y ) = m a x ( s o l v e ( x − 1 , y ) , s o l v e ( x , + 1 , y ) , s o l v e ( x , y − 1 ) , s o l v e ( x , y + 1 ) ) + 1 solve(x,y)=max(solve(x-1,y),solve(x,+1,y),solve(x,y-1),solve(x,y+1))+1 solve(x,y)=max(solve(x−1,y),solve(x,+1,y),solve(x,y−1),solve(x,y+1))+1
可以记忆每个solve(x,y),减少递归次数。
代码:
#include<iostream>
using namespace std;
const int N = 300+5;
int r,c;
int dp[N][N],a[N][N];
int dx[] = {
0,1,0,-1},dy[] = {
1,0,-1,0};
int solve(int i, int j){
if(dp[i][j]!=0) return dp[i][j];
int ans=1;
for(int k = 0; k < 4; k++){
int x=i+dx[k],y=j+dy[k];//遍历四个方向
if(x>=1 && x<=r && y>=1 && y<=c && a[x][y]<a[i][j]){
ans = max(solve(x,y)+1,ans);
}
}
dp[i][j]=ans;//记忆
return ans;
}
int main(){
cin>>r>>c;
for(int i = 1; i <= r; i++){
for(int j = 1; j <= c; j++){
scanf("%d",&a[i][j]);
}
}
int res=0;
for(int i = 1; i <= r; i++){
for(int j = 1; j <= c; j++){
res = max(res,solve(i, j));//找全局最大值
}
}
cout<<res<<endl;
return 0;
}