滚动数组(简单说明)
·
1.前提概要
-
滚动数组是一种能够在动态规划中降低空间复杂度的方法,有时某些二维dp方程可以直接降阶到一维,在某些题目中甚至可以降低时间复杂度,是一种极为巧妙的思想。
-
简要来说就是通过观察dp方程来判断需要使用哪些数据,可以抛弃哪些数据,一旦找到关系,就可以用新的数据不断覆盖旧的数据量来减少空间的使用。
2.先以斐波那契数列为例
#include<bits/stdc++.h>
using namespace std;
int main()
{
int a[3];
a[0] = 1;
a[1] = 1;
for(int i = 1;i <= 35;i++)
{
a[2] = a[0] + a[1];
a[0] = a[1];
a[1] = a[2];
}
printf("%d\n",a[2]);
return 0;
}
-
通过观察斐波那契数列方程 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n−1)+f(n−2),
-
我们可以发现,实际上只需要前两个递推的数求和即可,于是我们可以使用数组的前三个位置来分别存贮数据,再用递推得到的新数据将旧数据覆盖。
-
这样我们本来需要用三十多个位置的数组,最终却只用了三个位置,大大减少了空间复杂度。对于某些只需要最终答案的题目,我们可以抛弃掉当中一些不必要存贮的数据,来减少空间的使用。
3.再以01背包为例
-
这里我们以HDU2602的bone collector题目为例子,对于每一个状态,我们都可以判断这次是取还是不取或是取不下。
-
枚举第 i i i个物品,枚举 j j j代表背包的体积。
-
若不取或者取不下,那么结果就是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]。
-
若取,就由前面的状态预留出 w e i g h t [ i ] weight[i] weight[i] 的体积再加上 i i i物品的价值,即 d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[i-1][j-weight[i]]+value[i] dp[i−1][j−weight[i]]+value[i]。
-
可得二维dp方程 : d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) :dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) :dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i])
我们先来看二维的dp矩阵,背包价值是{1,2,3,4,5},对应的体积是{5,4,3,2,1};
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 0 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 3 | 3 |
3 | 0 | 0 | 0 | 3 | 3 | 3 | 3 | 5 | 5 | 5 | 5 |
4 | 0 | 0 | 4 | 4 | 4 | 7 | 7 | 7 | 9 | 9 | 9 |
5 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | 12 | 12 | 14 |
以 d p [ 4 ] [ j ] dp[4][j] dp[4][j] 到 d p [ 5 ] [ j ] dp[5][j] dp[5][j] 为例,此时第5个物品的体积为1,价值为5
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
4 | 0 | 0 | 4 | 4 | 4 | 7 | 7 | 7 | 9 | 9 | 9 |
5 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | 12 | 12 | 14 |
-
我们可以清楚的观察到,dp方程递推过程是不断地由上一行的数据传递到下一行,即一层状态向另一层状态转移
-
比如 d p [ 5 ] [ 10 ] dp[5][10] dp[5][10] 的状态是由 d p [ 5 ] [ 10 ] = m a x ( d p [ 4 ] [ 10 ] , d p [ 4 ] [ 9 ] + 5 ) = 14 dp[5][10]=max(dp[4][10],dp[4][9]+5)=14 dp[5][10]=max(dp[4][10],dp[4][9]+5)=14 递推得到的
-
也就是说当我们递推到 d p [ i ] [ j ] dp[i][j] dp[i][j]时,对于那些只要求最终最佳答案的情况来说,只需要 i − 1 i-1 i−1 这行的数据即可,至于上面的 i − 2 , i − 3 , . . . , 1 i-2,i-3,...,1 i−2,i−3,...,1 都是不需要的数据。
-
所以我们可以只用一维数组 d p [ j ] dp[j] dp[j] 来记录数据 d p [ i ] [ j ] dp[i][j] dp[i][j] 的状态,在更新的过程中不断用新的数据 d p [ j ] ( d p [ i ] [ j ] ) dp[j](dp[i][j]) dp[j](dp[i][j]) 覆盖掉旧的数据 d p [ j ] ( d p [ i − 1 ] [ j ] ) dp[j](dp[i-1][j]) dp[j](dp[i−1][j])。
4.为什么 j j j维度在01背包是逆序,完全背包是正序呢
-
我相信会有很多同学对01背包第二维j为什么是倒着递推有疑惑。
-
假设我们从正序进行,当 i = 5 i=5 i=5, j = 9 j=9 j=9时,那么此时 d p [ 9 ] dp[9] dp[9] 的状态是由前面的 d p [ 8 ] dp[8] dp[8] 和 d p [ 9 ] dp[9] dp[9] 状态递推得到的,那么现在 d p [ 9 ] dp[9] dp[9] 此时存贮的是 d p [ 5 ] [ 9 ] dp[5][9] dp[5][9] 的状态,那么也就不能保证 d p [ 5 ] [ 10 ] dp[5][10] dp[5][10] 的递推成功( d p [ 5 ] [ 10 ] dp[5][10] dp[5][10] 状态应该是由 d p [ 4 ] [ 10 ] dp[4][10] dp[4][10] 和 d p [ 4 ] [ 9 ] dp[4][9] dp[4][9] 递推得到的)
-
但正序的做法却意味着可以多次调用前面的数据,相当于多次取用物品,也就是完全背包(物品可取次数为无限次)的思路了。
那该怎么确保不覆盖 d p [ 9 ] dp[9] dp[9]存贮的数据 d p [ 4 ] [ 9 ] dp[4][9] dp[4][9]呢,那么倒序就起作用了。
-
假设当 i = 5 , j = 10 i=5,j=10 i=5,j=10 时, d p [ 10 ] dp[10] dp[10]内的数据存贮的是 d p [ 4 ] [ 10 ] dp[4][10] dp[4][10] 的数据,由于 j j j从尾部开始枚举, d p [ 10 ] dp[10] dp[10] 就会由 d p [ 9 ] dp[9] dp[9] 和 d p [ 10 ] dp[10] dp[10] 递推得到( d p [ 9 ] dp[9] dp[9] 存贮的是 d p [ 4 ] [ 9 ] dp[4][9] dp[4][9] , d p [ 10 ] dp[10] dp[10] 存贮的是 d p [ 4 ] [ 10 ] dp[4][10] dp[4][10]),那么此时 d p [ 10 ] dp[10] dp[10] 的值就更新为了 d p [ 5 ] [ 10 ] dp[5][10] dp[5][10]。
-
然后依次类推,因为是倒序的缘故,那么接下来枚举的 j j j 都要比先前的 j j j 要来的小,所以当某个位置更新数据时,并不会调用其位置后面的数据,一定会是先调用其位置之前的旧数据,然后再将当前位置的旧数据覆盖,也就保证了数据更新的有序性。
5.小结
6.具体代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3+10;
int t,n,v;
int dp[maxn];
int value[maxn];
int weight[maxn];
int main()
{
scanf("%d",&t);
while(t--)
{
memset(dp,0,sizeof dp);
scanf("%d %d",&n,&v);
for(int i = 1;i <= n;i++)
scanf("%d",&value[i]);
for(int i = 1;i <= n;i++)
scanf("%d",&weight[i]);
// for(int i = 1;i <= n;i++) 原始二维dp方程
// for(int j = 0;j <= v;j++)
// {
// if(j >= weight[i]) //若取得下,则可以选择取或不取
// dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
// else
// dp[i][j]=dp[i-1][j];
// }
for(int i = 1;i <= n;i++) //使用滚动数组优化后的dp方程
for(int j = v;j >= weight[i];j--) //倒序保证数据更新的有序性,保证只取一次,正序则是完全背包的写法
dp[j]=max(dp[j],dp[j - weight[i]] + value[i]);
printf("%d\n",dp[v]);
}
return 0;
}
更多推荐
所有评论(0)