参考博客https://blog.csdn.net/yoer77/article/details/70943462
以下都是按上面链接中博客学习过程的心得总结
1、第一部分
递推公式中,绝不能使用未定义的值(相当于等式后面的东西在上几次被赋值过)
递归公式中,从大到小递归,得到最小解后回溯,因此合法。但是在递推公式中,若也是从大到小推,如
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]) 则出现dp[i-1][j]未被定义的状态,不合法。这也就是为什么顺推的原因
2、压缩空间
由于dp[i][j] 的值仅仅受到dp[i-1] 的影响, 因此压缩成一维数组.
以轮次去理解
for i = 1 to n
for j = W to w[i]
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
逆推的原因,等式右边的值,相当于dp[i-1][j]和dp[i-1][j-w[i]]。为什么?因为执行过程中,右边dp[j]和dp[i-w[i]] 保留的是上轮的值。试想若是顺推,那么本轮用到的值,就是本轮修改过的,那么就和上轮无关了
从此开始讨论的递推都是一维数组形式
dp[j]的意义,也就是容量为j的时候,取得最大的价值
3、细节讨论,背包最终是否装满
通过初始化来区分,初始化相当于确立合法的解。
1)若要求背包恰好装满,则只初始化dp[0]=0,其他dp[other] = -1。也就是不合法状态,你并不知道这种容量是否能由条件刚好凑出来,因为没装满不合法。而容量是0的时候价值是0,就符合恰好装满条件,而其他的解必定是由之前的合法解推出来的,因此在初始化的时候确定为-1。
2)若可以不正好装满,则统统初始化成0。因为任何容量的背包都是合法的。
#include <iostream>
#include <cstring>
#define MAXN 10000
using namespace std;
int dp[MAXN];
int w[MAXN] = {0, 2, 1, 3, 2};
int v[MAXN] = {0, 3, 2, 4, 2};
int W = 5, n = 4;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) { // i从1开始,递增
for (int j = W; j >= 0; j--) { // j按递减顺序填表
if (j < w[i]) {
dp[j] = dp[j];
}
else {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
}
return dp[W];
}
int main() {
cout << solve(n, W) << endl;
return 0;
}
4、完全背包问题(每个物品数量不限)
分成两种情况
1)不选i dp[i][j] = dp[i-1][j]
2)选i dp[i][j] = dp[i][j-w[i]]+v[i]
解释:由于不知道到底装几个i,因此先装入一个,把问题转化成装入一个i后的最大值情况(状态转移了)
5、完全背包空间压缩
for i = 1 to n
for j = 0 to W
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
为什么是顺推?文章原话 完全背包问题第二层的j正序循环,因为dp[i][j] 需要用到dp[i][j−w[i]] , 只用一维数组保存的话,按j正序循环,就能保证计算dp[j] 时,旧的dp[j] 的值就等于dp[i−1][j] 的值,dp[j−w[i]] 已经计算过,且对应的就是二维数组中dp[i][j−w[i]] 的值。
个人理解:公式中dp[j] = max(dp[j], dp[j-w[i]] + v[i]), 等式右边的dp[j] 实际上是上一轮的dp[i-1][j] , 由于是顺推dp[j-w[i]]也是已经被计算出来的。
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN];
int w[MAXN] = {0, 3, 4, 2};
int v[MAXN] = {0, 4, 5, 3};
int W = 7, n = 3;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[j] = dp[j];
}
else {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
}
return dp[W];
}
int main() {
cout << solve(n, W) << endl; // 10
return 0;
}
6、多重背包
相当于才成很多个01背包,第i 个物品有 mi 个, 等价于有mi 个相同的物品。但直接拆分成 mi 件物品并不是最好的方法。我们可以利用二进制来拆分。例如 m1=13=20+21+22+6 ,我们将第一种物品共13件,拆分成 20,21,22,6 这四件
const int N = 100, W = 100000;
int cost[N], weight[N], number[N];
int dp[W + 1];
int knapsack(int n, int w)
{
for (int i = 0; i < n; ++i)
{
int num = min(number[i], w / weight[i]);//每个物品最多能有num个
for (int k = 1; num > 0; k*=2)//拆成2^0 , 2^1, 2^2...也是选和不选两种抉择
{
if (k > num) k = num;//k就是2^0.2^1....
num -= k;
for (int j = w; j >= weight[i] * k; --j)//01背包,把k个物件当成一个整体
dp[j] = max(dp[j], dp[j - weight[i] * k] + cost[i] * k);
}
}
return dp[w];
}
7、优化多重背包
(1)单调队列的概念。https://blog.csdn.net/justmeh/article/details/5844650
例题 滑动窗口,给定序列,和窗口大小k,求每次滑动的时候窗口内最大值
队首存放value最大的,向队尾递减。加入一个新元素时,执行两步操作 1)队尾中有小于本次值的,全出队 2)队首中,有下标已经小于本次窗口l的(已经在窗口之外了) 出队。 然后输出队首的值。
代码如下
#include<iostream>
#include<queue>
using namespace std;
struct Node
{
int val;
int index;
};
void GetMax(int *numSequence,int len, int *result,int k)
{
Node *que = new Node[len];
int head = 0;
int end = 0;
for(int i=0;i<len;i++)
{
Node tmp;
tmp.val = numSequence[i];
tmp.index = i;
while(end!=0 && que[end].val<=numSequence[i])
--end;
++end;
que[end] = tmp;
while(end!=0 && que[head].index<i-k+1)
++head;
result[i] = que[head].val;
}
delete []que;
}
int main()
{
int len, k;
cin>>len>>k;
int *numSequence = new int[len];
int *maxResult = new int[len];
for(int i=0;i<len;i++)
cin>>numSequence[i];
GetMax(numSequence,len,maxResult,k);
for(int i=k-1;i<len;i++)
cout<<i<<": "<<maxResult[i]<<endl;
delete[]numSequence;
delete[]maxResult;
numSequence = NULL;
maxResult = NULL;
return 0;
}
2)