关于滚动数组的笔记(第一篇博客)
背景
最近在学习动态规划==真是个麻烦的小机灵鬼
做到的一道小小的题
P1048 采药
做法
(1)直接DP(一堆离题的废话)
这道题很显然就是一道01背包问题但是它的确是我做的第一道DP题
所以首先设计状态和写出状态转移方程
这俩货是真的难搞
一开始我设计的是记f(x)为采了前x种药材的最大价值,但是,采了药之后会消耗一定的时间,这样就导致了未来再采药时会受到采了第x株药的影响。
例如总时间是10,有消耗时间和价值分别为(6,6),(7.8),(4,3)的三种药材,如果用上面的设计状态,那么f(1)就会等于6(暂时不管时间),这样按理说f(2)应该等于第一和第二种药材的价值之和,也即是f(2)= 13。
很显然这是错的因为这里没有考虑到f(1)所消耗的时间对f(2)的影响,所以很明显这种设计状态并不满足无后效性原则。
“未来与过去无关”,这就是无后效性。
怎么办呢
“如果有元素导致了违背无后效性,那么就将它考虑进设计状态——rxz”
所以后来改成了f(n,k),表示在拥有的时间为k的时候,装入前n件药品的剩余时间的最大值。瞬间就看着很正确了吧!
接着就是状态转移方程,在这里很容易得到,如果不选第n个药品,那就转移成f(n-1,k),如果选,则转移成f(n-1,k-v[ n ],然后在里面选一个最大值即可。
即
f(n,k)=max(f(n-1,k),f(n-1,k-v[ n ]))
(其中n表示前n个物品,k表示所拥有的时间)
然后利用这个式子打出表格模拟一下刚刚的数据,每一个格子表示在能装入前n个物品时(行),拥有的时间为k时(列),药品最大价值。
n\k | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 |
2 | 0 | 0 | 0 | 0 | 0 | 6 | 8 | 8 | 8 | 8 |
3 | 0 | 3 | 3 | 3 | 3 | 6 | 8 | 9 | 11 | 11 |
这个就是模拟出来的结果,最后答案是11
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 10005;
int t[N],val[N];
int dp[N][N];//dp[i][k]表示在时间为k的情况下采前n株药材的最大价值
int main() {
int T,M;
cin>> T>> M;
for (int i = 1; i <= M; i++) {
cin>> t[i]>> val[i];
}
for (int i = 1; i <= M; i++) {
for (int k = 1; k <= T; k++) {//这里的k从T到1和从1到T都是可以的
if (k >= t[i])
dp[i][k] = max(dp[i-1][k-t[i]]+val[i],dp[i-1][k]);
else if (k < t[i])
dp[i][k] = dp[i-1][k];
cout << dp[i][k]<<" ";
}
cout << endl;
}
cout << dp[M][T];
return 0;
}
重点!滚动数组
由状态转移方程可以发现,每次要计算某一格的价值,只与上一行的某格有关(意思就是不与上两行或三行,下一行或两行有关),所以每计算完一行,上一行的数据就没用了!!!
所以我们可以联想到,能否只用一维的数组,将其中的数据通过某种手段处♂理完之后,再覆盖回原来的数组,实现空间复杂度的降低呢?答案是肯定的。
方法即主角——滚动数组,它只是一种实现方式,而非真实存在滚动数组这一数据结构,因其数组不断一行一行的更新,所以才叫滚动数组。
那么问题来了,要怎么实现呢
说难不难,但细节之多也不能谓之简单,代码直接附上。。。
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 10005;
int t[N],val[N];
int dp[N];//dp[k]表示在时间为k的情况下采前n株药材的最大价值
int main() {
int T,M;
cin>> T>> M;
for (int i = 1; i <= M; i++) {
cin>> t[i]>> val[i];
}
for (int i = 1; i <= M; i++) {
for (int k = T; k >= 1 ; k--) {
if (k - t[i] >= 0)
dp[k] = max(dp[k-t[i]]+val[i],dp[k]);
else if (k < t[i])
dp[k] = dp[k];
cout << dp[k]<<" ";
}
cout << endl;
}
cout << dp[T];
return 0;
}
关键是在k的那个循环里,这里的k从1和从1到T都是不可以的!!!因为要是直接从小到大的话会导致原来保存下来的数组出现偏差,即非最优解覆盖原来的最优解覆盖了上一层的最优解之后,之后本应利用上一层的最优解,的新容量最优解使用了更大的数据
直接把错误结果附上
0 0 0 0 0 6 6 6 6 6
0 0 0 0 0 6 8 8 8 8
0 3 3 6 6 9 9 12 12
很显然第一,二行暂且没有问题,但是在第三行的第四列开始就出现了错误,原因很简单,就是利用了错误的数据进行计算,f(n,k)=max(f(n-1,k),f(n-1,k-v[ n ])),f(3,4)应该等于max(f(2,4),f(2,2)+ 3)=3,在滚动数组中则会使用f(2)的值来计算,但是使用滚动数组的话,在计算f(3,2)的时候已经将它需要使用的元素修改成了3,所以导致了这场悲剧的发生。。。
那么为什么倒过来就是正确的呢,表格附上。。。
n\k | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 6 | 6 | 6 | 6 | 6 | 0 | 0 | 0 | 0 | 0 |
2 | 8 | 8 | 8 | 8 | 6 | 0 | 0 | 0 | 0 | 0 |
3 | 11 | 11 | 9 | 8 | 6 | 3 | 3 | 3 | 3 | 0 |
重点是最后一行数据(n=3的时候)的来源,
第一个元素,k=10的时候,f(3,10)应该等于max(f(2,8)+ 3,f(2,10))也就是处理前一维数组中的第一个(k=10)元素的值8或第三个元素(k=8)的值8加上第三件药品的价值3的最大值,显然是后者,11。
第二个元素,k=9的时候,f(3,9)为max(f(2,7)+3,f(2,9)),即一维数组中第二个元素的值8或第四个元素的值8加上3的最大值,显然也是11。
第三个元素,k=8的时候,f(3,8)是max(f(2,6)+3,f(2,8)),即一维数组中第三个元素的值8或第五个元素的值6加上3的最大值,显然是9。
不难发现,在第三行中,每次计算k号元素的值时,我们所利用到的,仅仅只是一维数组中第原来的第k号元素和第k-2号元素,每次修改的是k号元素的值,并不会影响到后面,简单来说,第一个元素是最强资本家,他从后面的小资本家榨取利益,这并不会影响后面的小资本家向小小资本家榨取利益。
而如果是从小到大,那么就是小资本家企图榨取大资本家的利益,结果到了后面,反而让大资本家全部再榨取回去了==
结尾
第一次写blog,第一次做DP,莫名其妙就写了这么多了,希望以后自己会过头来看的时候能认认真真的看!!!自己该有的模拟自己好好模拟!!!