动态规划—搞懂01背包和完全背包算法

01背包问题

有N件物品和一个容量为V的背包。第i件物品的费用是v[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

要么拿,要么不拿

  • dp解决方法

关键就在于找到它的最优子问题,物品为N个,体积为V,我们需要取二维状态的dp

将前i个物品放入体积为j的背包中可以获得的最大价值->dp[i][j]

只有单件物品就只需要考虑放或者不放,如果放入,体积就需要减去v[i],价值就加上w[i]

比如我们想要知道前5个物品放入体积为9的背包中最大价值是多少,物品id为0-4

也就是求dp[4][9],需要知道dp[3][j],0<j<V,然后再对物品4进行选取,根据01背包状态转移方程计算dp[4][9]最大价值;

要知道dp[3][j],我们就要知道dp[2][j],再对物品3进行选取,根据01背包状态转移方程计算dp[3][j]最大价值;

要知道dp[2][j],我们就要知道dp[1][j],再对物品3进行选取,根据01背包状态转移方程计算dp[2][j]最大价值;

要知道dp[1][j],我们就要知道dp[0][j],再对物品3进行选取,根据01背包状态转移方程计算dp[0][j]最大价值;

dp[i][0]初始化为0(0<=i<N),体积为0最大价值为0

  • 计算流程
public int napzack01_first(int []w,int []v,int V,int N){
        int [][]dp = new int[N][V+1];
        for(int i=0;i<N;i++)
            for(int j=1;j<=V;j++)
                if(j>=v[i])
                    if(i==0) dp[0][j] = w[i];
                    else dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
                else
                    dp[i][j] = dp[i-1][j];
        return dp[N-1][V];
    }

第一次优化:二维数组优化为一维数组

我们可以从表格发现,当前行的数据只与上一行的数据有关,所以我们只要每次循环后确保数组保存了上一次计算的结果

问题以及解决:

我们从当前循环来看,计算体积为5的dp值最多只需要0-4的上一次计算的结果,如果顺序从小到大更新的话,我们计算5时,此时之前的0-4的值都被更新了,不是上一次计算的结果(而是当前计算的结果),而后面的需要上一次前面的数据;所以我们不能从头开始,而应该从最后开始更新,从后往前,这样可以保证优先更新体积大的值而让体积小的值保留上一次就算的结果。

//1维01背包
    public int napzack01_second(int []w,int []v,int V,int N){
        int []dp = new int[V+1];
        for(int i=0;i<N;i++)
            for(int j=V;j>=0;j--)
                if(j>=v[i])
                    if(i==0) dp[j] = w[i];
                    else dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
        return dp[V];
    }

全部放满01背包:

我们之前讨论了一种是不要求全部放满的01背包,我们再看如果要求全部放满会有什么不同

如果我们把物品表换成这样:

如果我们想要全部放满,就拿不到物品4,即使它价值50,但是我们想要的是要把背包装满,这就涉及到初始化时的问题

我们需要为每一次循环放入物品到j的背包中设置一个能否放满的标志,每次判断这个标志来检查当前物品放入能否使得背包装满

我们只需要在初始化时把一维数组的1-V初始化为-1,表示当前0个物品放入背包,不能装满这些体积的背包,把数组dp[0]正常设置为0,表示体积为0的背包可以被0个物品装满;然后我们在计算数组值时先判断dp[j-v[i]]是否为-1,为-1说明当前物品放入无法装满背包,则不进行修改数组的值;只有不为-1才能继续放入,说明当前物品放入可以放满体积为j的背包

//全部放满01背包
    public int napzack01_fourth(int []w,int []v,int V,int N){
        int []dp = new int[V+1];
        for(int i=0;i<dp.length;i++){
            dp[i] = -1;
        }
        dp[0] = 0;
        for(int i=0;i<N;i++)
            for(int j=V;j>=0;j--)
                if(j>=v[i]&&dp[j-v[i]]!=-1)
                    dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
        return dp[V];
    }

完全背包

有N件物品(每个物品都有无限个)和一个容量为V的背包。第i件物品的费用是v[i],价值是w[i]。求解将哪些物品(每个物品都可以装多个)装入背包可使价值总和最大。

  • 优化前

我们每种物品最多有选取V/v[i]个,我们可以再多一次循环,计算选取0-V/v[i]个的最大价值

时间复杂度:每种物品有V/v[i]个,共需要求解NV中状态,时间为O(NVΣ(V/v[i]))

空间复杂度:O(N)

状态转移方程:

dp[j] = max{dp[j-1],dp[j-k*v[i]]+k*w[i]}

code

//完全背包
    //状态转移方程 dp[j] = dp[j-k*v[i]]+k*w[i]
    public int napzack_complete(int []w,int []v,int N,int V){
        int dp[] = new int [V+1];
        for(int i=0;i<N;i++)
            for(int j=0;j<=V;j++)
                for(int k=0;k*v[i]<=j;k++)
                    dp[j] = Math.max(dp[j-1],dp[j-k*v[i]]+k*w[i]);
        return dp[V];
    }
  • 优化为01背包

我们记得在优化01背包时,我们为了获取到上一次计算的值,我们选择从后往前计算,但是完全背包正好相反,这才是它此昂要的,完全背包因为需要累计多个同一物品的值,前一次计算可能是1个、2个等等,下一次j变化了以后,计算的可能是3个或者更多,所以我们需要保存实时计算出来的多个同一物品的最大价值,我们选取从前往后的顺序,这样每次前面计算的我们都可以在j增大以后累加获得更多个同一物品的最大价值(根据状态转移方程可知,我们计算一个位置的最大价值只需要当前位置的上一次计算的值和当前次循环内更前面的值)

例如:

在我们计算第2个物品dp[5]的时候,物品2的体积为2,价值为5,我们需要上一次计算也就是第一个物品的dp[5]的值,还需要dp[5-2]=dp[3]的值,dp[3]我们在本次循环内计算dp[5]之前就已经算过了,dp[3]可能选了一个物品2,也可能没有选,我们计算dp[5]就根据这个dp[3]的大小在进行选取,就可以进行多次选取。

根本上就是把一类物品转化为多个一种物品

我们不需要体积从0开始计算。而只需要在每个物品的循环内从当前物品的最小个数1开始,也就是v[i](不需要0个是因为初始化的时候已经把体积为0的dp值设置为0了)

参数依然使用

code

//把完全背包优化为01背包
    public int napzack_comlete01(int []w,int []v,int N,int V){
        int dp[] = new int[V+1];
        for(int i=0;i<N;i++)
            for(int j=v[i];j<=V;j++)
                dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
        return dp[V];
    }