这篇文章将背包问题和动规做一个整理复习。
背包问题的本质上是一个选择问题,即通过选择来得到最大价值或各种各样的性质。
动态规划基础知识
DP 问题我们一般的思路是先确定解法的基本形式,再在它的基础上做优化。
首先我们对DP问题的分析流程做一下定义, 对于DP问题我们要确定的要素主要为
(1) 状态表示 :dp 元素表示的是哪个集合
(以01背包问题为例: (i, j) 代表了只考虑在 1~i 范围内选择物品,并且体积小于等于 j 的所有状态的集合)
存的数是表示集合的哪一个属性(目标)
(以01背包问题为例:dp(i, j) 代表了(i, j)代表的所有状态集合中最大价值)
动态规划一般包含(MIN, MAX, COUNT)三种目标。
(2) 状态计算(状态转移方程):对应的是集合的划分,如何将当前的集合dp( i,j )划分成几个更小的子集。
(1) 01背包问题
(1.1) Solution 1 : 二维动态规划
了解了这些以后我们就可以基于这个思路对01背包问题做一些分析:
(1) 当
(2) 当
#include <iostream>
#include <vector>
#define N 1100
using namespace std;
int main(void){
int n, m;
int v[N], w[N];
int dp[N][N];
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++){
if(j-v[i] >= 0) dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
else dp[i][j] = dp[i-1][j];
}
}
cout << dp[n][m];
return 0;
}
(1.2) Solution 2: 状态压缩 : 一维动态规划
dp问题的所有优化都是在代码上做等价变形 , 和问题本身无关,只和代码逻辑有关。 01背包在时间复杂度上没法再优化, 从空间上我们其实还能做一些优化。
- 首先其实可以发现整个转移方程中对于 i 这一维,只用到了i -1, 所以我们其实并不需要记录所有的dp[i][..],相反只需要用单个变量记录即可(滚动数组)。这样我们可以得出
- 在去除dp 数组的 i 这一维后,我们碰到了一些问题:对于 j 这一维因为 j - v[i] < j , 所以实际上 dp[j-v[i]] 这个状态已经被计算过了,这代表了什么呢,每次循环开始时 dp[j] 记录的是dp[i-1][j]的信息,而循环结束更新后 dp[j] 记录的则为dp[i][j]的信息,如果我们从前往后循环,那么我们每次更新时dp[j] = dp[j - v[i]]+w[i] 相当于原先二维动态规划时的 dp[i][j] = dp[i][j-v[i]]+w[i], 然而实际的目标 为 dp[i][j] = dp[i-1][j-v[i]]+w[i], 那么怎么去修正这一点呢? 我们从大到小循环, 这样则保证了每一次的 i 都是由 i - 1 推出来的 (dp[j-v[i]]在本轮尚未更新过,所以依旧记录的是dp[i-1][j-v[i]]的数值)。
#include <iostream>
#include <vector>
using namespace std;
const int N = 1100;
int main(void){
int n, m;
vector<int> v(N,0), w(N,0);
vector<int> dp(N,0);
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 = m; j >= v[i]; j--){
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
}
}
cout << dp[m] << endl;
return 0;
}
(2) 完全背包问题
问题描述:
有 N 种物品和一个容量是 V 的背包, 每种物品都有无限件可用。
第 i 种物品的体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
和01背包问题不同的是这里我们每种物品都可以使用无数次, 而01背包问题则将每个物品的使用限定在了一次。完全背包问题的状态表示可以与01背包问题完全一致, 即 [i][j]代表在1~i的范围内考虑物品, 体积为 j 的所有状态 集合。
(2.1) Solution 1 :
#include <iostream>
#include <cstdio>
#define N 1100
using namespace std;
int main(void){
int n, m;
int v[N], w[N];
int dp[N][N];
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 <= j/v[i]; k++)
dp[i][j] = max( dp[i-1][j], dp[i-1][j - k*v[i]] + k*w[i] );
}
}
cout << dp[n][m] << endl;
return 0;
}
(2.2) Solution 2 : 状态压缩
优化思路:
状态转移方程的核心部分为:
将k展开后:
对于
不难发现, 因为max算子在范围内可以加减 :
这样我们就化简掉 k 的使用,发现状态转移方程与01背包问题非常相似除了第二项 i 与 i -1的区别,然后用与01背包问题相似的方法我们可以将dp数组再次缩减为一维的。(注: 由于这里需要计算的是dp[i][j-v[i]] 根据我们前一节得出的结论,这里只需要从前往后更新便能符合要求。)
#include <iostream>
#include <cstdio>
using namespace std;
#define N 2010
int main(void){
int n, m;
int v[N], w[N];
int dp[N];
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 = v[i]; j <= m; j++){
dp[j] = max( dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m] << endl;
return 0;
}
(3) 多重背包问题
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
( 3.1 ) Solution 1 : 二维动态规划暴力解法
#include <iostream>
#include <cstdio>
#define N 110
using namespace std;
int main(void){
int n, m;
int v[N], w[N], s[N];
int dp[N][N];
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin>> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k <= s[i] && 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] << endl;
return 0;
}
( 3.2 ) Solution 2 : 状态压缩,二进制优化
多重背包问题没办法用完全背包问题的思路优化。
这里引出一种新的思路: 将一个多重背包问题化为01背包问题, 即如果一个物品有s个,那么实际上我们可以将其拆为s份,每一份为一个新的物品,物品的重量与价值保持不变,对于每件物品只有装入和不装入两种选择,这样再重新组织后,一个多重背包问题就被转化为01背包问题。
然而直接拆分成s份的复杂度实际上是很高的:即 S x N。
下一步就引出了我们要使用的方法,用二进制的方法来拆分物品。
我们的目标实际上是想要用最少的数,组合出
这里以7 为例, 从0 到 7 一共有8个数, 每个数选和不选有两种选择, 也就是说如果我么有3个状态即可以表示8个数 ,那么这个问题的优化下界应当等于向上取整。
这代表了什么呢, 我们可以用二进制表示来压缩拆分状态, 用3个数组合出0~7的任意一个数:
这样我们将原先个数为
然而这里还有一个问题是对于非二的整次幂的数该怎么办, 以数字10 为例子, 我们不能将10 分解成 1,2,4 或1,2,4,8 (包含了所有15以内数,超过我们的物品数量范围) 。
设
因为前k个数能表示的状态为0 ~ 2^k, 加上最后一个数后能表示的状态即为
这样我们的优化步骤就完成了,剩下的我们已经将多重背包问题转化为01背包问题所以用01背包问题的解法即可。
代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#define N 20100
using namespace std;
int main(void){
int n, m, v_t, w_t, s_t;
vector<int> v, w;
vector<int> dp(N);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d%d%d", &v_t, &w_t, &s_t);
for(int t = 1; s_t >= t; t *= 2){
v.push_back(t*v_t);
w.push_back(t*w_t);
s_t -= t;
}
if(s_t > 0){
v.push_back(s_t*v_t);
w.push_back(s_t*w_t);
}
}
for(int i = 0; i < v.size(); i++)
for(int j = m; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j - v[i]]+w[i]);
cout << dp[m] << endl;
return 0;
}
(4)分组背包问题
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
分组背包问题实际上与上面的01背包十分相似, 区别在于分组背包DP状态数组第一维表示的是, 只考虑第 1 ~ i 组。
Solution :
#include <iostream>
#include <algorithm>
#define N 110
using namespace std;
int main(void){
int n, m;
int v[N][N], w[N][N], s[N];
int dp[N];
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i] ;
for(int j = 0; j < s[i]; j++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--)
for(int k = 0; k < s[i]; k++)
if(j-v[i][k] >= 0)
dp[j] = max( dp[j], dp[j-v[i][k]] + w[i][k]);
cout << dp[m] << endl;
return 0;
}