DP系列刷题中问题小总结
概述
DP一种常用的算法思想,可难可易,非常灵活。
解决通法:
求解DP问题主要有三步:(废话)
- 定义状态
- 状态转移
- 算法实现
对于DP问题,主要需要找出状态的定义以及状态的转移。
DP问题全家桶(苟若刷题遇见的题型)
- [简单DP] (
and复杂DP🐶) - 区间DP
- 树形DP
- 数位DP
- 状压DP
- 递推
以下内容为本人(苟若)在学习DP过程中的一些小总结。
简单DP
简单DP是一些经典的问题,一般可以很直观的表示并转移状态。
1. 硬币问题
1.1 最少硬币问题
Problem Description:
设有n
种不同面值的硬币,现要用这些面值的硬币来找钱。硬币可以使用无限次,有T
组输入,每次输入一个m
,对任意钱数0≤m≤20001
,设计一个用最少硬币找钱m的方法。
解题思路:考虑本问题所求属性为最少的硬币找钱。
假设硬币为[1,5,10,15,20]
,需要找钱数目为m
, 那么m
可以由什么状态转移过来呢?
很显然,m=m-1+1=m-5+5=m-10+10=......
,问题就转换成了找钱m-1,m-5.....
的子问题。
显而易见的,我们可以定义状态dp[i]
:找钱数为i所需要的最少硬币。
状态转移: dp[i]=dp[i-money_type]+1
由于有多种途径,所求属性为最小值,则
dp[i]=min(dp[i],dp[i-money_type]+1)
Solve-Code:
#include<iostream>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
void initial() {
for (int i = 0; i < Money; i++)
dp[i] = INF;
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = money_type[i]; j < Money; j++) {
dp[j] = min(dp[j], dp[j - money_type[i]] + 1);
}
}
}
int main() {
int T,m;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> money_type[i];
}
cin >> T;
initial();
for (int i = 0, x; i < T; i++) {
cin >> x;
cout << dp[x] << endl;
}
return 0;
}
求具体方案
对于具体方案,显然的,我们可以在状态转移的过程中添加转移路径(即方案),在更新最小值的同时同步更新选择的硬币面值。
Solve-Code:
#include<iostream>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
int dp_path[Money] = { 0 };
void initial() {
for (int i = 0; i < Money; i++)
dp[i] = INF;
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = money_type[i]; j < Money; j++) {
if (dp[j] > dp[j - money_type[i]] + 1) {//需要转移
dp[j] = dp[j - money_type[i]] + 1;
dp_path[j] = money_type[i];
}
}
}
}
void Print(int s) {//输出方案
while (s) {
cout << dp_path[s] << ' ';
s -= dp_path[s]; //回到上一个状态
}
}
int main() {
int T,m;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> money_type[i];
}
cin >> T;
initial();
for (int i = 0, x; i < T; i++) {
cin >> x;
cout << dp[x] << endl;
cout << "组成方案为:" << endl;
Print(x);
cout << endl;
}
return 0;
}
1.2 所有方案硬币的组合
Problem Description:
设有n
种不同面值的硬币,现要用这些面值的硬币来找钱。硬币可以使用无限次,有T
组输入,每次输入一个m
,对任意钱数0≤m≤20001
,求用硬币找钱m的总方案数。
解题思路:考虑本题所求属性为方案总数
由1.1
相信你一定有所感悟,在不断更新最小值的过程中不是已经枚举出来了所有方案的呀!
于是状态表示 dp[i]:钱数为i的找钱方案
,显然当需要找钱数为m
时,有面值为1
,5
时,面值m
的方案为m-1
与·m-5
方案的总和。
转移当然是和上面(一样样):dp[i]+=dp[i-money_type]
上述对代码的修改很少
dp
初始化为0
,dp[0]=1
, 状态转移过程中 dp[j]+=dp[j-money_type[i]]
#include<iostream>
#include"cstring"
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
void initial() {
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = money_type[i]; j < Money; j++) {
dp[j] += dp[j - money_type[i]];
}
}
}
int main() {
int T,m;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> money_type[i];
}
cin >> T;
initial();
for (int i = 0, x; i < T; i++) {
cin >> x;
cout << dp[x] << endl;
}
return 0;
}
1.3 背包问题
背包问题是DP下又一大类子问题。
1.3.1 01背包
Problem Description:有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
动态规划是不断决策求最优解的过程,0-1 背包
即是不断对第 i
个物品的做出决策,0-1
正好代表不选与选两种决定。而维度有当前背包,以及背包的容量。
考虑本题所求属性:最大价值
状态dp[i][j]
定义:前 i
个物品,背包容量 j
下的最优解(最大价值):
状态转移:选:dp[i][j]
= dp[i - 1][j - v[i]]
+ w[i]
(若选择了该物品,自然需要为该物品提供出空间)
不选:dp[i][j]
= dp[i - 1][j]
(未选择该物品,不需要为该物品提供空间)
Solve-Code:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int dp[MAXN][MAXN]; // dp[i][j], 前i个物品j体积的最大价值
int main() {
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++){
if(j < v[i])
dp[i][j] = dp[i - 1][j]; // 当前背包容量装不进第i个物品,则价值等于前i-1个物品
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);// // 能装,需进行决策是否选择第i个物品
}
cout << dp[n][m] << endl;
return 0;
}
01背包问题可以降为一维动态规划,想了解的朋友可以参见
01背包问题的一维优化
1.3.2 完全背包问题
Problem Description:有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
这里仍然采用最朴素的三维思路:对于每一个物品,我们需要加入物品选择数量的考量,故会引入第三重循环去遍历装入该物品的可能数量去更新dp[i][j]
由于思路较为简单,不加赘述.
Solve-Code:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
using namespace std;
const int N = 10010;
int dp[N][N];
int v[N],w[N];
int main(){
int n,m;
cin>>n>>m;
for(int i = 1 ; i <= n ;i ++){
cin>>v[i]>>w[i];
}
for(int i = 1 ; i<=n ;i++)
for(int j = 0 ; j<=m ;j++) {
for(int k = 0 ; k*v[i]<=j ; k++)
dp[i][j] = max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
cout << dp[n][m];
return 0;
}
完全背包问题的一维优化
除此之外还有多重背包问题、混合背包问题、二位费用背包问题、分组背包问题、有依赖的背包问题、背包求方案数的问题、背包求具体方案问题。
背包问题是典型的DP问题,读者应当尝试自己动手完成系列问题的推导与求解。
这里引用经典博文详细介绍了背包问题的求解。
背包九讲
背包问题较多,选取部分一维优化代码请参见:
背包问题
1.4 最长公共子序列
Problem Description:对于序列X与序列Y,求X与Y的公共子序列
状态表示:dp[i][j]
:子序列Xi段与子序列Yj段的最长公共子序列的长度,显而易见的,dp[X.lenth][Y.lenth]
即为所求解。
状态转移:
dp[i][j]=dp[i-1][j-1]+1 if(X[i]==Y[j])
当两者相等时,显然我们可以[i-1,j-1]
像后扩展一位
dp[i][j]=max(dp[i][j-1],dp[i+1][j]) if(X[i]!=Y[j])
当两者不相等时,可以尝试独立加入X[i]
,此时为dp[i][j-1]
,
独立加入Y[j]
,此时为dp[i-1][j]
,得出转移方程。
Solve-Code:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 1000;
int dp[N][N];
int main() {
string X,Y ;
cin >> X >> Y;
X = " " + X;
Y = " " + Y; //小东西便于代码编写
for(int i=1;i< X.size();i++)
for (int j = 1; j < Y.size(); j++) {
if (X[i] == Y[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
for (int i = 1; i < X.size(); i++) {
for (int j = 1; j < Y.size(); j++) {
cout << dp[i][j] << ' '; //打印各部分状态,得到转移表
}
cout << endl;
}
cout << f[X.size()-1][Y.size()-1];
return 0;
}
1.5 最长公共子串
最长公共子序列和最长公共子串不是一回事儿。子序列即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。子串是给定串中任意个连续的字符组成的子序列称为该串的子串。
Problem Description:对于字符串S1与序列S2,求S1与S2的最长公共子序列
求解属性:公共子串最长长度,对于S1与S2各含一个维度。
状态表示:f[i][j]
:以s1[i]
和s2[j]
作为结尾的最长公共子串长度
状态转移:
if (s1[i] == s2[j]) f[i][j] = f[i-1][j-1] + 1; //可以向后扩展
else f[i][j] = max(0, f[i][j]); //注意与子序列不同,子串必须保持连续,否则f[i][j]=0;
Solve-Code
//最长公共子串 s1,s2;
//f[i][j]:以s1[i]和s2[j]作为结尾的最长公共子串长度
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 10000;
int f[N][N];
int main() {
string s1, s2;
cin >> s1 >> s2;
int n = s1.size(), m = s2.size();
//递推下标是从1开始的
for (int i = 0; i < n; i++)
if (s2[0] == s1[i]) f[i][0] = 1;
for (int j = 0; j < m; j++)
if (s1[0] == s2[j]) f[0][j] = 1;
for(int i=1;i<n;i++)
for (int j = 1; j < m; j++) {
if (s1[i] == s2[j]) f[i][j] = f[i-1][j-1] + 1;
else f[i][j] = max(0, f[i][j]);
}
cout << f[n-1][m-1];
return 0;
}
作者修养生息中准备下一章…
小试牛刀
想练习可以AC以下题目:
Coin Change
超级楼梯
Robberies
最大报销额