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(n1)+f(n2)
  • 我们可以发现,实际上只需要前两个递推的数求和即可,于是我们可以使用数组的前三个位置来分别存贮数据,再用递推得到的新数据将旧数据覆盖
  • 这样我们本来需要用三十多个位置的数组,最终却只用了三个位置,大大减少了空间复杂度。对于某些只需要最终答案的题目,我们可以抛弃掉当中一些不必要存贮的数据,来减少空间的使用

3.再以01背包为例

  • 这里我们以HDU2602的bone collector题目为例子,对于每一个状态,我们都可以判断这次是取还是不取或是取不下。
  • 枚举第 i i i个物品,枚举 j j j代表背包的体积。
  • 若不取或者取不下,那么结果就是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][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[i1][jweight[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[i1][j],dp[i1][jweight[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 i1 这行的数据即可,至于上面的 i − 2 , i − 3 , . . . , 1 i-2,i-3,...,1 i2,i3,...,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[i1][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.小结

  • 对于动态规划题目来说,我们可以先写出最原始的dp方程,再通过观察dp方程,使用滚动数组进行优化,我们需要思考如何更新数据和覆盖数据来达到降维的目的(可能需要很长的时间思考,不过熟能生巧)。

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;
}

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐