数组是最常用的数据结构之一,现在我们对数组的下标进行特殊处理,使每一次操作仅保留若干有用信息,新的元素不断循环刷新,看上去数组的空间被滚动地利用,此模型我们称其为滚动数组。其主要达到压缩存储的作用,一般常用在DP类题目中。因为DP题目是一个自下而上的扩展过程,我们常常用到是连续的解,而每次用到的只是解集中的最后几个解,所以以滚动数组形式能大大减少内存开支。
一、二维滚动数组
例1:0-1背包问题
问题描述:
有 n 件物品x1, x2, …, xn , 每件物品有一个价值和一个重量,分别记为:
v1,v2, …vn
w1,w2, …wn
其中所有的 wi 均为整数。 现有一个背包,其最大载重量为m,要求从这n件物品中任取若干件(这些物品要么被装入要么被留下)。问背包中装入哪些物品可使得所装物品的价值和最大?
我们很容易得出状态转移方程:f(I,j) = max{f(I-1, j-w[I]) + v[I], f(I-1, j)}
例如,容量为10,有5个物体,
重量为3 5 1 9 7
价值为11 28 6 49 35
从推导过程中可以看出,第I阶段的状态值只与I-1阶段有关,以前的数据保存在那里已经毫无意义。为此,我们就想到了利用滚动数组进行优化。
具体实现时,我们可以将f数组的空间由[0…n,0..m]改为[0..1,0..m],空间复杂度由O(n*m)下降到O(2*m)。在存储过程中,我们设定一个变量c,则语句段为:
c:=0;
for i:=1 to n do begin
c:=1-c;
for j:=1 to m do begin
f[c,j]:=f[c,j-1];
if j-t[i]>0 then
if f[1-c,j-t[i]]+p[i]>f[c,j] then f[c,j]:=f[1-c,j-t[i]]+p[i];
end;
end;
这样,二维数组f的第一个下标值的取值就在0,1之间循环变化,实现了数组的滚动存储。
二、一维滚动数组
其实,在二维滚动数组的基础上,我们还可以优化为一维滚动数组,但此时滚动的方向尤其重要。例如上例的01背包我们可以降为一维,但在递推f[j]时应按m到0的顺序,这样才能保证推f[j]时f[j-w[i]]保存的是状态f[i-1,j-w[i]]的值。如上表中的第3阶段的数据如下表,在递推第4阶段时,最后状态f[10]的值是由f[10]的值和f[1]+49比较而来,此时,要保证f[1]的值是上一阶段的结果,若方向向反,就很难保证此值不被更新。
相应语句段为:
for i:=1 to n do
for j:=m downto 0 do
if (j>=wi) and (f[j-wi]+pi>f[j]) then
f[j]:=f[j-wi]+pi;
这样,新产生的数据将不断覆盖旧数据,实现了一维数据的滚动效果。
三、MOD滚动数组
滚动数组应用的条件是基于递推或递归的状态转移中,反复调用当前状态前的几个阶段的若干个状态,而每一次状态转移后,都有固定个数的状态失去作用。
滚动数组便是充分利用了那些失去作用的状态的空间,填补新的状态。
Mod 滚动数组主要应用在需要调用多个前面的阶段的状态的情况。
例2:楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,编一程序计算共有多少种不同的走法。
经过分析,此题的状态转移方程为:f[I]=f[I-1]+f[I-2],f[1]=1,f[2]=2
此时的f[I]只跟f[I-1]和f[I-2]有关,所以用滚动数组可优化空间,但如何实现调用两个前面的阶段的状态情况?就要用到MOD滚动数组。语句段为:
f[1]:=1;
f[2]:=2;
for I:=3 to n do
f[I mod 3]:=f[(I-1) mod 3]+f[(I-2) mod 3];
这样,在程序执行过程中只利用了三个空间f[0],f[1]和f[2],实现了数组的滚动效果。
滚动数组实际是一种节约空间的办法,可根据具体题目要求选择相应的滚动数组进行优化。