dp系列
手把手教你dp:01背包问题(递归改动态规划DP)
手把手教你dp:摘花生问题(递归改动态规划DP)
手把手教你dp:蓝桥杯-地宫寻宝(递归改动态规划DP)
1 背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0 < N,V ≤ 1000
0 < vi,wi ≤ 1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
2 递归求解
2.1 思路
对于每一件物品我们都可以选择拿或者不拿,因此可以递归选择每个位置的物品拿或者不拿。
(1)当背包剩余容量小于当前位置物品的体积时,我们只能选择不拿当前位置的物品
(2)当背包剩余容量小于或者等于当前位置物品的体积时,我们可以选择拿当前位置物品与不拿当前位置物品
a.如果拿当前位置物品,则背包内物品总价值相应增加,背包容积相应减小,继续确定下一个位置。
b.如果不拿当前位置物品,则背包内物品总价值和容积保持不变,继续确定下一个位置。
以上过程的终止条件是当前位置指向最后一个物品的下一个位置或者当前背包容积为0,即为递归终止条件
2.2 递归函数代码
w是代表当前背包中物品的总价值
cur是当前物品的位置
v是当前背包的剩余容量
int w;
int process(int cur, int v){
if(cur == n + 1 || v == 0) return 0;
if(s[cur].x > v){
return w + process(cur + 1, v);
}
return w + max(s[cur].y + process(cur + 1, v - s[cur].x), process(cur + 1, v));
}
2.3 递归版本完整代码
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N= 1010;
int n, v;
PII s[N];
int w;
int process(int cur, int v){
if(cur == n + 1 || v == 0) return 0;
if(s[cur].x > v){
return w + process(cur + 1, v);
}
return w + max(s[cur].y + process(cur + 1, v - s[cur].x), process(cur + 1, v));
}
int f(int v){
return process(1, v);
}
int main()
{
cin >> n >> v;
for(int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;
cout << f(v) << endl;
return 0;
}
3 递归改动态规划
3.1 思路
对于一个无后效性问题,只要写出了递归版本,就可以在递归的基础上改出DP,甚至不需要关注原问题,只需要关注递归代码即可
(1)首先我们需要判断是哪几个变量确定了一种状态,不难发现,上面递归中的cur和v可以唯一确定一种状态,因此我们的dp表是二维的。
(2)确定cur和v的范围,在二维表中我们以v代表行,cur代表列,显然v的范围[0, v],cur 的范围是[1, n+1](n是物品的个数,我们假设物品从1~n编号,因为递归的终止掉条件是cur == n+1,因此cur的范围需要包括n+1)。
(3)确定目标结果在二维表中的位置,显然,我们的目标位置是(v, cur) = (v, 1)。
(4)根据递归终止条件,确定表的边界状态,有上述递归代码可得,当cur = n+1时,整列全为0;当v=0时,整行全为0,因此我们的二维表的初始状态为一行全为0,最后一列全为0。
(5)对于任意一个位置,由上述递归代码,推出状态转移方程。对于任意一个位置(v, cur) = (i, j),如果 j 号物品的体积大于背包容积v,则dp[i][j] = dp[i][j+1];
if(s[cur].x > v)
return w + process(cur + 1, v);
如果 j 号物品的体积小于或者等于背包容积v,则dp[i][j] = max(s[j].w + dp[i-s[j].v][j+1], dp[i][j+1])。
return w + max(s[cur].y + process(cur + 1, v - s[cur].x), process(cur + 1, v));
3.2 dp表求解代码
通过上述五步分析,由于表中第一行和最后一列的值都已确定为0,因此我们只需要从第二行倒数第二列开始求解,知道求解到目标位置(v, cur) = (v, 1)。
for(int i = n; i >= 0; i--){
for(int j = 1; j <= v; j++){
if(s[i].x > j) dp[i][j] = dp[i+1][j];
else dp[i][j] = max(s[i].y + dp[i+1][j-s[i].x], dp[i+1][j]);
}
}
求解完dp表,目标位置上的值就是我们的结果。
3.3 dp完整代码
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N= 1010;
int n, v, t;
PII s[N];
int w;
int dp[N][N];
int main()
{
cin >> n >> v;
for(int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;
for(int i = n; i >= 0; i--){
for(int j = 1; j <= v; j++){
if(s[i].x > j) dp[i][j] = dp[i+1][j];
else dp[i][j] = max(s[i].y + dp[i+1][j-s[i].x], dp[i+1][j]);
}
}
cout << dp[1][v] << endl;
return 0;
}
4 递归改dp的步骤
(1)dp表的维度:即状态由几个变量唯一确定。
(2)确定表的各个维度的取值范围。
(3)确定目标值在dp表中的位置。
(4)根据递归终止条件,确定dp表的初始状态。
(5)确定dp表中任意一个位置的值与表中其他位置的依赖关系,即状态转移方程。