第五十五天 --- 洛谷P1616(完全背包)+洛谷P1048(0-1背包)
前言
洛谷专题来啦呀,希望大家和俺多多讨论,假期也要冲冲冲!
题目一要求:
整体思路
每一株草药,我们都有两种选择,要或者不要,并且题目没说无限次取用,典型0-1背包,0-1背包具体分析以及滚动数组空间优化详见我另一篇博客滚动数组解决0-1背包问题。
具体代码
注:
1、滚动数组本质就是我们把所有的状态存在一起,不用单独存放,单独用一个维度标识存放位置了,所以原来的第一个维度i表示处理第几个物品,那个维度就无意义了,所以留下了第二个维度j,千万别理解错了。
2、有个理解滚动数组解决0-1背包为啥要逆向取j,(实在不理解就手动走一遍DP过程)因为正向会发生反复取用同一件物品的情况,而0-1背包不允许反复取用,所以逆着取用。
3、Runtime Error: 数组开小了,除零错误,大数组定义在函数内导致了栈区耗尽,指针错误,程序抛出未接收异常
#include <algorithm>
#include <iostream>
#include <stdio.h>
using namespace std;
const int CNT = 1500;
int times[CNT] = {};//时限
int val[CNT] = {};//价值
int T, M;//总时间和草药总数
int dp[CNT] = {};//存储状态数组
int main() {
cin >> T >> M;
for (int i = 1; i <= M; i++) {
scanf("%d%d", ×[i], &val[i]);
}
for (int i = 1; i <= M; i++) {
for (int j = T; j >= times[i]; j--) {//滚动数组解决0-1背包,必须从后向前找,防止一个物品被多次取用。
dp[j] = max(dp[j], dp[j - times[i]] + val[i]);
}
}
cout << dp[T];
return 0;
}
(所有代码均已在力扣上运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
时间复杂度:O(N^2) (这个我算的不一定对,有错误请大家帮我指出,谢谢大家)
题目二要求:
整体思路 :
1、一定先理解好0-1背包,这道题都是在0-1背包基础之上来的。
2、这个题乍一看和0-1背包极其相似,就是一点不同,可以无限取用,所以我们在考虑转移方程的时候,把它取用次数考虑进去就完了。
具体代码一:未进行时间优化
1、开long long是为了防止越界,因为本体数据量很大
2、因为取用次数可以无限,所以我们先提前算好取用次数,然后在转移方程里面加进取用次数就好了
3、再利用滚动数组优化空间,把第一位i干掉,那么核心代码段以及转移方程就有了
#include <algorithm>
#include <iostream>
#include <stdio.h>
#define int long long
using namespace std;
//笨方法,没有利用到滚动数组本质,硬做题,枚举我选取某一件物品的数量,TLE错误
const int CNT = 1e7;
int times[CNT] = {};
int val[CNT] = {};
int counts[CNT] = {};//记录每种草药最多能拿多少
int T, M;
int dp[CNT] = {};
signed main() {
scanf("%lld%lld", &T, &M);
for (int i = 1; i <= M; i++) {
scanf("%lld%lld", ×[i], &val[i]);
counts[i] = T / times[i];
}
for (int i = 1; i <= M; i++) {
for (int k = 1; k <= counts[i]; k++) {//枚举取用数量
for (int j = T; j >= k * times[i]; j--) {//在滚动数组0-1背包基础上改进而来,考虑了取用个数
dp[j] = max(dp[j], dp[j - k * times[i]] + k * val[i]);
}
}
}
printf("%lld", dp[T]);
return 0;
}
错误之处
通过上述结果来看,我们设计的这个算法本质没问题,但是时间复杂度很明显过大(O(N^3)),出了TLE错误,所以只需要针对时间优化即可。
具体代码二:利用该滚动数组性质进行时间优化
1、我们想一想为啥子0-1背包再用滚动数组的时候从后向前,不正是因为如果从前到后的话会出现反复取用的情况吗,但是我本题恰好就是反复取用。
2、我们不妨看个例子:(该过程借鉴自洛谷题解)
首先dp数组初始化全为0:给定物品种类有4种,包最大体积为5,数据来源于题目的输入
v[1] = 1, w[1] = 2
v[2] = 2, w[2] = 4
v[3] = 3, w[3] = 4
v[4] = 4, w[4] = 5
i = 1 时: j从v[1]到5
dp[1] = max(dp[1],dp[0]+w[1]) = w[1] = 2 (用了一件物品1)
dp[2] = max(dp[2],dp[1]+w[1]) = w[1] + w[1] = 4(用了两件物品1)
dp[3] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] = 6(用了三件物品1)
dp[4] = max(dp[4],dp[3]+w[1]) = w[1] + w[1] + w[1] + w[1] = 8(用了四件物品1)
dp[5] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] + w[1] + w[1] = 10(用了五件物品)
i = 2 时:j从v[2]到5
dp[2] = max(dp[2],dp[0]+w[2]) = w[1] + w[1] = w[2] = 4(用了两件物品1或者一件物品2)
dp[3] = max(dp[3],dp[1]+w[2]) = 3 * w[1] = w[1] + w[2] = 6(用了三件物品1,或者一件物品1和一件物品2)
dp[4] = max(dp[4],dp[2]+w[2]) = 4 * w[1] = dp[2] + w[2] = 8(用了四件物品1或者,两件物品1和一件物品2或两件物品2)
dp[5] = max(dp[5],dp[3]+w[2]) = 5 * w[1] = dp[3] + w[2] = 10(用了五件物品1或者,三件物品1和一件物品2或一件物品1和两件物品2)
i = 3时:j从v[3]到5
dp[3] = max(dp[3],dp[0]+w[3]) = dp[3] = 6 # 保持第二轮的状态
dp[4] = max(dp[4],dp[1]+w[3]) = dp[4] = 8 # 保持第二轮的状态
dp[5] = max(dp[5],dp[2]+w[3]) = dp[4] = 10 # 保持第二轮的状态
i = 4时:j从v[4]到5
dp[4] = max(dp[4],dp[0]+w[4]) = dp[4] = 10 # 保持第三轮的状态
dp[5] = max(dp[5],dp[1]+w[4]) = dp[5] = 10 # 保持第三轮的状态
上面模拟了完全背包的全部过程,也可以看出,最后一轮的dp[m]即为最终的返回结果。
3、综上所术,我们没有必要专门计算他到底最多能取某种药草几株,我们只需要在滚动数组时候从前到后,利用滚动数组自身性质,自然而然地就会把一种药草采多株的情况包含在内,自然就解决了矛盾,并且因为不用枚举可采个数,时间复杂度降低到O(N^2),也不会TLE了。
#include <algorithm>
#include <iostream>
#include <stdio.h>
#define int long long
using namespace std;
const int CNT = 1e7;
int times[CNT] = {};
int val[CNT] = {};
int T, M;
int dp[CNT] = {};
signed main() {
scanf("%lld%lld", &T, &M);
for (int i = 1; i <= M; i++) {
scanf("%lld%lld", ×[i], &val[i]);
}
for (int i = 1; i <= M; i++) {
for (int j = times[i]; j <= T; j++) {//改变枚举方向即可
dp[j] = max(dp[j], dp[j - times[i]] + val[i]);
}
}
printf("%lld", dp[T]);
return 0;
}
(所有代码均已在洛谷上运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
时空复杂度: O(nm)
Sum Up
1、0-1背包和完全背包问题就差在能不能无限次取用物品上。
2、滚动数组不仅可以优化空间,还可以优化完全背包问题的时间复杂度,将之降低很多
3、0-1背包不可以无限次取用,所以根据滚动数组性质,我们从后向前枚举
4、完全背包支持无限次取用,我们就利用滚动数组性质从前到后枚举,且可降低时间复杂度。
5、综上:(滚动数组优化后)
0-1背包模板
完全背包模板:没必要死记硬背,理解好0-1背包和滚动数组性质,自然而然地可以推出完全背包模板,可见0-1背包非常的重要。