关于滚动数组的笔记(第一篇博客)

背景

最近在学习动态规划==真是个麻烦的小机灵鬼

做到的一道小小的题
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,莫名其妙就写了这么多了,希望以后自己会过头来看的时候能认认真真的看!!!自己该有的模拟自己好好模拟!!!

posted @ 2021-11-01 17:10  tsrigo  阅读(29)  评论(0编辑  收藏  举报