背包九讲(灵魂版)

文章目录

背包九讲

博客里的题目、内容都在这个链接里

题库 - AcWing

背包问题是一类组合优化问题,是动态规划中的

一个重要而特殊的模型。核心是,将一堆有一定体积、价值的物品,选择若干放进容量有限的背包,是总价值最大。

在不了解DP之前,面对这样的选择问题,也许反应就是暴力枚举不同选择,找到最优,也就是搜索。

而动态规划的巧妙就体现出来了,究其如何巧妙,各位先看01领会一下

01背包

看模板题:

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i i i 件物品的体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

暴力枚举每一种选择,肯定T,

既然叫01背包 ,顾名思义,每一个物品,只有两种可能,选/不选 。而一个数字的选择与否自然会影响最终的结果。问题的求解一直在动,看似很难有一个确定的方式得出答案

这时候如果有一个确定的 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],表示对于前 i − 1 i-1 i−1 个物品,体积为 j j j时,可容纳的最大价值

当我们考虑下一个物品 i i i
{ d p [ i ] [ j ] = d p [ i − 1 ] [ j ] (不选择该物品) d p [ i ] [ j ] = d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] (选择) \left\{\begin{matrix} dp[i][j]= dp[i-1][j] (不选择该物品) \\ \\ dp[i][j]=dp[i-1][j-v[i] ]+w[i](选择)\end{matrix}\right. ⎩ ⎨ ⎧dp[i][j]=dp[i−1][j](不选择该物品)dp[i][j]=dp[i−1][j−v[i]]+w[i](选择)

  • 不选择物品 i i i, 对于体积 j j j, 所容纳的最大价值也就是只考虑前 i − 1 i-1 i−1的结果
  • 选择物品 i i i, 已知 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],同样的体积只考虑了前 i − 1 i-1 i−1,此时要在其中加入 i i i, 就要在 j j j中让出 v [ i ] v[i] v[i]个位置, 所以,用 w [ i ] + d p [ i − 1 ] [ j − v [ i ] ] w[i]+dp[i-1][j-v[i] ] w[i]+dp[i−1][j−v[i]](只考虑前 i − 1 i-1 i−1个物品,体积为 j − v [ i ] j-v[i] j−v[i]时的最大价值) 。

最后将最大的赋值给 d p [ i ] [ j ] dp[i][j] dp[i][j]。

这样不就解决了不确定性的问题 ,只要我们得到确定最优的 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],问题就解决了。** d p [ N ] [ V ] dp[N][V] dp[N][V]**自然就是答案了。

那如何得知 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]呢

相同的,向前查找 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],这就类似dfs了,一直搜索到 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0](数组要怎么初始化赋值?后面会说)。搜索完就要回溯赋值了。

而动态规划就像是回溯 ,得到答案的过程。用式子表达出来,就是上面的递推公式

问题的答案其实就是一张二维表(dp思想就蕴含其中)

针对这个样例

c++ 复制代码
4 5
1 2
2 4
3 4
4 5
物品i(v,w) ||体积 j 0 1 2 3 4 5(选,不选)
0 0 0 0 0 0 0
(1,2) 0 2 2 2 2 max(2,0)
(2,4) 0 2 4 6 6 max(6,2)
(3,4) 0 2 4 6 6 max(8,6)
(4,5) 0 2 4 6 6 max(6,8)

~表中将j=5时的选择表示出来,以供理解~

朴素代码

c++ 复制代码
const int N=1010;
int f[N][N];//只看前i个物品,总体积为j情况下,总价值最大是多少
signed main()
{
    IOS
    int n,k;
    cin>>n>>k;
     vector<PII> a(n+1);
    fir(i,1,n)
    cin>>a[i].fi>>a[i].se;
    for(int i=1;i<=n;i++)
    {
      for(int j=1;j<=k;j++)
      { 
        f[i][j]=f[i-1][j]; //1. 不选第i个,f[i][j]=f[i-1][j]
          if(j>=a[i].fi)
        f[i][j]=max(f[i][j],f[i-1][j-a[i].fi]+a[i].se);   
        //2. 选择第二个,f[i][j]=f[i-1][j-v[i]];
      }  
    }
    int res=0;
    fir(i,0,k)//可以直接输出f[n][k],原因看在下面
    res=max(res,f[n][i]);
    cout<<res<<'\n';
}

在上述代码,我们将 f [ i ] [ j ] f[i][j] f[i][j]放全局,初始化为0,此时 f [ n ] [ k ] f[n][k] f[n][k],表示的就是物品总体积<=k时的最大价值 。所以可以直接输出 f [ n ] [ k ] f[n][k] f[n][k],

而如果只 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0,其他赋值为负无穷,表示的就是物品总体积==k时的最大价值。这时候就要像代码中一样循环比较一下。

这里有点难理解,建议按 上面样例,用两种赋值方式模拟一遍。

全赋值为0

f [ 1 ] [ 3 ] = m a x ( f [ 0 ] [ 3 ] = 0 , f [ 0 ] [ 3 − 1 ] + 2 ) = 2 f[1][3]=max(f[0][3]=0,f[0][3-1]+2)=2 f[1][3]=max(f[0][3]=0,f[0][3−1]+2)=2

除第一个,赋值为负无穷

f [ 1 ] [ 3 ] = m a x ( f [ 0 ] [ 3 ] = − I N F , f [ 0 ] [ 3 − 1 ] + 2 ) = − I N F f[1][3]=max(f[0][3]=-INF,f[0][3-1]+2)=-INF f[1][3]=max(f[0][3]=−INF,f[0][3−1]+2)=−INF

也就是当物品体积不为3时,是无法成功赋值的,也就是说它无法从** f [ 0 ] [ 2 ] 递推出 f [ 1 ] [ 3 ] f[0][2]递推出f[1][3] f[0][2]递推出f[1][3]**

而 f [ 1 ] [ 1 ] = m a x ( f [ 0 ] [ 1 ] , f [ 0 ] [ 1 − 1 ] + 1 ) = 1 , 从 f [ 0 ] [ 0 ] 可以推出来 f [ 1 ] [ 1 ] f[1][1]=max(f[0][1],f[0][1-1]+1)=1,从f[0][0]可以推出来f[1][1] f[1][1]=max(f[0][1],f[0][1−1]+1)=1,从f[0][0]可以推出来f[1][1]

可以这样说,一个数是由前面的数递推出来的,

全赋值为0,任何体积都可以向后递推, 总体积自然不一定等于k。而第二种赋值,就只允许从0开始赋值,这时 d p [ n ] [ k ] dp[n][k] dp[n][k]体积就一定是K,当然它的值也很可能时-INF。

一维优化代码

c++ 复制代码
const int N=1010;
int f[N];//只看前i个物品,总体积为j情况下,总价值最大是多少
signed main()
{
    IOS
    int n,k;
    cin>>n>>k;
     vector<PII> a(n+1);
    fir(i,1,n)
    cin>>a[i].fi>>a[i].se;
    for(int i=1;i<=n;i++)
    {
      for(int j=k;j>=a[i].fi;j--)
      { 
        f[j]=max(f[j],f[j-a[i].fi]+a[i].se);   
      }  
    }
    cout<<f[k]<<'\n';
}

将j倒序循环就不需要用二维,可以这样理解记忆。我们选择物品i-1后,如果正着遍历j,从小到大,会提前将 f [ j − a [ i ] . f i ] f[j-a[i].fi] f[j−a[i].fi]更新为考虑前i个物品的结果,这样向后遍历就相当在考虑前i个物品结果的基础上,再次考虑第i个物品

所以这里倒序

那如果,每一个物品有无限个呢?这时是不是就可以直接改成正序了?

是的。这就是下面的完全背包

完全背包

和01背包不一样的地方在,每一个物品可以无限次选择。

一维优化的代码,也就是上面提到的,直接将 j正序循环

而能体现它思想的朴素写法是这样来的:

max中依次时当前物品选择0,1,2,3.......的最大价值

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , f [ i − 1 ] [ j − 2 v ] + 2 w , f [ i − 1 ] [ j − 3 v ] + 3 w , . . . . . ) f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,f[i-1][j-3v]+3w,.....) f[i][j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w,f[i−1][j−3v]+3w,.....)

​ f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , f [ i − 1 ] [ j − 3 v ] + 2 w , f [ i − 1 ] [ j − 4 v ] + 3 w , . . . . . ) f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w,f[i-1][j-4v]+3w,.....) f[i][j−v]=max(f[i−1][j−v],f[i−1][j−2v]+w,f[i−1][j−3v]+2w,f[i−1][j−4v]+3w,.....)

观察两个式子,可以将2式代入1式,由此便得

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i ] [ j − v ] ) f[i][j]=max(f[i-1][j-v],f[i][j-v]) f[i][j]=max(f[i−1][j−v],f[i][j−v])

朴素代码

c++ 复制代码
const int N=1010;
int a[N],b[N],f[N][N];
signed main()
{
    IOS
    int n,k;
    cin>>n>>k;
    fir(i,1,n)
    cin>>a[i]>>b[i];
    fir(i,1,n)
    fir(j,1,k)
    {
        f[i][j]=f[i-1][j];
        if(j>=a[i])
        f[i][j]=max(f[i][j],f[i][j-a[i]]+b[i]);//f[i][j-v],错位相减
    }
    cout<<f[n][k]<<"\n";
     
}

一维优化代码

c+ 复制代码
const int N=1010;
int a[N],b[N],f[N];
signed main()
{
    IOS
    int n,k;
    cin>>n>>k;
    fir(i,1,n)
    cin>>a[i]>>b[i];
    fir(i,1,n)
    fir(j,a[i],k)
    {
        
        f[j]=max(f[j],f[j-a[i]]+b[i]); 
    }
    cout<<f[k]<<"\n";
     
}

多重背包I 模板题

多重背包是每件物品,有确定的数量。

这时候,可以在,j循环里面再开个循环,分别是选择0,1,2......s个

依次更新j体积下的最大价值。

代码

c++ 复制代码
const int N=1010;
int a[N],b[N],f[N];
signed main()
{
    IOS
    int n,k,v,w,s;
    cin>>n>>k;
    fir(i,1,n)
    {
        cin>>v>>w>>s;
        for(int j=k;j>=0;j--)//倒序,对照01
        {
            for(int l=1;l<=s&&j>=l*v;l++)
            f[j]=max(f[j],f[j-v*l]+w*l);
        }
    }
    cout<<f[k]<<'\n';
     
}

多重背包 II 二进制优化

按照上面的思路,三重循环,很容易超限。

对于给定的数量s,朴素法就是:1,1,1,1,1,分解成s个1。这就变成了01背包问题。分别是该物品选择0,1,2......s;

那我们能不能换一种分解方式,同样也能组合成(0-s任意一种选择),使分解完的数量减少

二进制很特殊,仅有01组成,解决问题简单,同时还能把问题大大优化。

s = 17 = 2 0 + 2 1 + 2 2 + 2 3 + 2 s=17=2^0+2^1+2^2+2^3+2 s=17=20+21+22+23+2

类似的,任意一个数都可以由2的幂次方+常数c组成。这就是纯废话,肯定能呀,

关键是,从1到s,任意一个数都可以由他们组成

如何理解呢,先看一串二进制数

1 0 1 1 1 1
1
1 0
1 0 0
1 0 0 0
1 0 0 0 0
C

表格中第一行是二进制数,其后每行是一个可分解出的 2 n 2^n 2n,由于仅有01组成,所以某个数字的加与否,决定了所组成数字某一位是0还是1.所以对于组合出所有需要的数字。

由于原二进制某个数位可能是零,这时候最后一行不一定就写 C = 2 n C=2^n C=2n,当然这样做,也能组合为所有数,得出答案。我们也可以 C = s − 2 0 − 2 1 − 2 2 . . . . . . 2 n − 1 C=s-2^0-2^1-2^2......2^ {n-1} C=s−20−21−22......2n−1,这样也可以组合成所有数。

将这些拆分出来的数,作为新的物品,每个物品只有一个。

现在就又变为了0 1背包。

代码

c++ 复制代码
const int N=2010;
int n,k,f[N];
signed main()
{
    IOS
    vector<PII> g;
    cin>>n>>k;
      for(int i=0;i<n;i++)
      {
          int v,w,s;
          cin>>v>>w>>s;
          for(int j=1;j<=s;j*=2)
          {
              s-=j;
              g.push_back({v*j,w*j});//转化成01背包
          }
            if(s>0)
            g.push_back({v*s,w*s});  
      }
        for(auto gg:g)
      {
          for(int j=k;j>=gg.fi;j--)
          f[j]=max(f[j],f[j-gg.fi]+gg.se);
      }
     cout<<f[k]<<endl;
}

多重背包 III 单调队列优化

有s个物品体积为v,价值为w.

到底存放几个更优呢?

按照朴素做法,在01背包中加一重循环(0~s),更新最优价值。

也就是

循环变量 含义
i i i 1 n 遍历每一个物品
j j j m v 更新每一个可容纳体积下最优价值
k k k 1 s&& k ∗ v < = j k*v<=j k∗v<=j 遍历可选择数量,更新最优

i i i,遍历每一个物品,这是必要的,无法优化。 k k k,一个物品放几次更优,可以采用上面的二进制优化,单调队列中不进行优化,也是需要逐个更新。

而 j j j,就一定要从m到v ? 每种容量都更新一遍?

那让我们看一看关于不同 j j j之间有没有什么联系

f [ i ] [ r + 3 v ] = m a x ( f [ i − 1 ] [ r + 3 v ] , f [ i − 1 ] [ r + 2 v ] + w , f [ i − 1 ] [ r + v ] + 2 w , f [ i − 1 ] [ r ] + 3 w ) f[i][r+3v]=max(f[i-1][r+3v],f[i-1][r+2v]+w,f[i-1][r+v]+2w,f[i-1][r]+3w) f[i][r+3v]=max(f[i−1][r+3v],f[i−1][r+2v]+w,f[i−1][r+v]+2w,f[i−1][r]+3w)

f [ i ] [ r + 4 v ] = m a x ( f [ i − 1 ] [ r + 4 v ] , f [ i − 1 ] [ r + 3 v ] + w , f [ i − 1 ] [ r + 2 v ] + 2 w , f [ i − 1 ] [ r + v ] + 3 w , f [ i − 1 ] [ r ] + 4 w ) f[i][r+4v]=max(f[i-1][r+4v],f[i-1][r+3v]+w,f[i-1][r+2v]+2w,f[i-1][r+v]+3w,f[i-1][r]+4w) f[i][r+4v]=max(f[i−1][r+4v],f[i−1][r+3v]+w,f[i−1][r+2v]+2w,f[i−1][r+v]+3w,f[i−1][r]+4w)

对于物品 i i i, 每次需要考虑放 0 , v , 2 v , 3 v , 4 v . . . 0,v,2v,3v,4v... 0,v,2v,3v,4v...我们总是用 j − k v j-kv j−kv去表示,现在反过来我们用 r + k v r+kv r+kv来表示 j j j

可以发现,当我们选择占用几个体积 v v v时,都是在体积 r r r的基础上添加

  • 之前 j j j是用来循环不同背包容量的

所以 r + v , r + 2 v , r + 3 v , r + 4 v . . . r+v,r+2v,r+3v,r+4v... r+v,r+2v,r+3v,r+4v...,全部都要循环一遍,不仅如此 ( r + 1 ) + v , ( r + 1 ) + 2 v , ( r + 1 ) + 3 v . . . (r+1)+v,(r+1)+2v,(r+1)+3v... (r+1)+v,(r+1)+2v,(r+1)+3v...,也需要遍历一遍。

总的来说, r ∈ [ 0 , v − 1 ] r\in [0,v-1] r∈[0,v−1],对于每一个 r , r + v , r + 2 v , r + 3 v , r + 4 v . . . . . . r,r+v,r+2v,r+3v,r+4v...... r,r+v,r+2v,r+3v,r+4v......都需要遍历。也就是不同j情况下,都考虑一遍放几个更优

看到这,是不是产生一点点想法了?这些 j j j有一个共同特点,对 v 取模皆为 r v取模皆为r v取模皆为r.

  • 现在用 j 代替 r j代替r j代替r ,遍历区间就是 [ 0 , v − 1 ] [0,v-1] [0,v−1],

这样上面两个式子,我们就可以放在一块儿去考虑了,因为他们都是在解决同一个问题, 在 r 上,放几个 v 更优 在r上,放几个v更优 在r上,放几个v更优。

每一个 j j j,可以看作一个队列,需要考虑两点

  1. 大小 s + 1 s+1 s+1 ( j , j + v , j + 2 v . . . j + s v j,j+v,j+2v...j+sv j,j+v,j+2v...j+sv)
  2. 单调性 (需要解决的问题,本身就是不同体积不同物品个数最优解,现在放到队列里,也自然是为了比较出最优解)

因为有固定大小,所以也可以联想到滑动窗口;用单调队列,直接代替了max函数比较;

这里用一维dp写的,但需要上一个物品选择后的状态来递推,所以用pre数组去存放。

更多细节,在代码注释中

代码

c++ 复制代码
#include <iostream>  。
#include <cstring>  
using namespace std;  
const int N = 20010;  

int dp[N], pre[N], q[N]; // 定义三个数组,dp用于存储DP状态,pre用于保存dp的上一轮状态,q用于单调队列
int n, m; // n表示物品数量,m表示背包容量

int main() {
    cin >> n >> m;  
    for (int i = 0; i < n; ++i)
     { // 遍历所有物品
        memcpy(pre, dp, sizeof(dp)); // 复制当前dp数组到pre数组,以便在更新dp数组时不会丢失之前的状态
        int v, w, s;                 // v表示物品体积,w表示物品价值,s表示物品数量
        cin >> v >> w >> s;  
        for (int j = 0; j < v; ++j) { // 遍历余数0-v
        
            int head = 0, tail = -1; // 初始化单调队列的头和尾,不同余数,不同类,开设一个新的队列
            
            for (int k = j; k <= m; k += v) //向背包里不断装v, 遍历所有可能的容纳总体积,
             { 
             
                // 控制窗口大小,进行移动。物品只有s个,大于s*v,从新的体积开始放
                if (head <= tail && k-q[head] >s*v) //队列不为空情况下,进行判断
                    ++head;
                    
            // dp[j+3v] = max(dp[j] + 3w,  dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])            
           // 保持队列的单调性, 现在体积为k,状态方程中,需要在max中找最大的,可以发现,体积每小一个v,就多加一个w
          //dp[q[tail]]+x*w,  dp[k]+y*w 两者就是max中的式子.dp[q[tail]]+x*w<dp[k]+y*w 变换为下面式子
    
                while (head <= tail && pre[q[tail]] +(k-q[tail])/v * w <= pre[k])
              
                { 
                   --tail;  //现在更优,去尾。单调队列控制,就不用max函数
                }
                
                // 如果队列不为空,使用队列头部的状态更新当前状态
                if (head <= tail)
                 dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w); 
               //dp[j+3v] = max(dp[j] + 3w,  dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
                // 将当前体积存入队列尾部
                q[++tail] = k;
            }
        }
    }
    cout << dp[m] << endl; // 输出背包能够装下的最大价值
    return 0; // 程序结束
}

混合背包

其实就是将上面三种混到一块儿。若干物品,有的只有一个(01);有的无数个(完全);有的确定数量(多重)

我们知道,解决多重背包,是将其分解为01背包。

这里我们也可以用二进制优化,转为01背包。

而完全背包,他的解决就是01背包中** j j j正序循环**。直接看代码

代码

c++ 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
vector< tuple<int,int,int> > f;
int dp[N];
int main()
{
    int n,m,v,w,s;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>v>>w>>s;
        if(s==-1) f.push_back({v,w,s});
        else if(s==0) f.push_back({v,w,s});
        else
        {
            for(int k=1;k<=s;k<<=1)
            {
                s-=k;
                f.push_back({v*k,w*k,-1});
            }
            if(s>0) f.push_back({v*s,w*s,-1});
        }
    }
    for(auto [a,b,c]:f)
    {
        if(c==-1)
        for(int i=m;i>=a;i--)
        {
            dp[i]=max(dp[i],dp[i-a]+b);
        }
        else
        for(int i=a;i<=m;i++)
        dp[i]=max(dp[i],dp[i-a]+b);
    }
    cout<<dp[m]<<'\n';
}

二维费用的背包问题

在原有体积的限制下,加了重量限制。

只需在加一重循环,用来限制重量,更新不同重量下的最优解。

为什么可以直接加一重循环,难道他俩不会影响结果?

我是这样理解的,当进行内层循环时,外层是一个固定的 j j j,此时相当于 j j j是一个无关紧要的定值。

这就联想到数学中的偏导,对某一变量求偏导,不就是将另一视作定值常数,两偏导互不影响,但可以共同组成全微分。

这里将两个循环套起来,不会影响正确性,反而可以共同求解二维费用背包问题。

代码

c++ 复制代码
vector<tup> f; 
int dp[1010][1010];
signed main()
{
    IOS
    int N,V,M,v,m,w;
    cin>>N>>V>>M;
    fir(i,1,N)
    {
        cin>>v>>m>>w;
        f.push_back({v,m,w});
    }
    for(auto [a,b,c]:f)
    {
         for(int j=V;j>=a;j--)//两重循环,将一个变量当成定值
         for(int k=M;k>=b;k--)
         {
             dp[j][k]=max(dp[j][k],dp[j-a][k-b]+c);
         }
    }
    cout<<dp[V][M]<<'\n';
}

分组背包

将物品进行分组,每组最多选择一个。求最大价值。

可以想到前面的多重背包的朴素代码,用了三个循环,第三个循环更新选择不同数量的最优价值。

这里也可以三个循环,第三个用来更新选择不同物品的最优价值

代码

c++ 复制代码
int dp[110],v[105],w[105];
signed main()
{
    IOS
    int n,m,c;
    cin>>n>>m;
    fir(i,1,n)
    {
        cin>>c;
        fir(j,1,c)
        cin>>v[j]>>w[j];
     for(int j=m;j>=1;j--)
        {
            fir(k,1,c)
            {
                if(j>=v[k])
                dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
            }
        }
    }
    cout<<dp[m]<<'\n';
}

有依赖的背包问题

在01背包中,我们遍历物品从一到 n n n,进行状态转移,但因为这是数形dp,我们从子节点到根进行状态转移更优。

采用dfs,在回溯时进行状态转移,这是因为

状态转移:一个节点的状态更新依赖于子节点的状态,回溯时进行状态转移是最佳时机

保持状态独立性: 确保只考虑子节点的状态,不受其他兄弟节点影响

自下而上的转移,根节点的状态就是答案。

有多个节点,又有体积控制,和01背包类似。不过当选择到该节点时,我们需要考虑他的子节点如何选择,分配多少体积,左还是右,全选还是全不选。我们只能做出一个选择。

这就可以想到分组背包,也是在一组物品中,最多选择一个。

我们就可以从分组背包的角度考虑这道题,每个节点是一个组,该节点子树所分配的不同体积为组内物品,因为子树的不同体积恰好可以反应不同的选择方案。(其实也可以和多重联系到一块,因为多重的朴素做法也是三重循环,第三层是考虑选几个更优。)

所以内层循环就是更新给子树分配不同体积的最优状态

那我们如何实现依赖性呢

我们在分配体积时,就需要给自身分配,再考虑子节点

因为我们是从子节点向根进行状态转移。也就是对于每一个dp阶段,他的父节点只能选取就好了。
这种题是背包类树形dp,以节点编号,作为dp的阶段,体积作为第二维状态,而在状态转移时,我们要处理的就是分组背包问题

代码

c++ 复制代码
#include<iostream>
#include<vector>
using namespace std;
//这里不是按子树来分组,而是按每棵子树的不同体积来分组。即我们选择的每种方案,每个子树的体积必然是确定的, 
int f[110][110];//f[x][v]表达选择以x为子树的物品,在容量不超过v时所获得的最大价值
vector<int> g[110];
int v[110],w[110];
int n,m,root;
//这里对于树中的每个节点来说,就是一个分组背包问题。每个子节点是一组物品,每个子节点的不同体积和每个体积所对应的最大价值,就是这个物品组中的物品。
void dfs(int x)
{
    for(int i=v[x];i<=m;i++) f[x][i]=w[x];//点x必须选,所以初始化f[x][v[x] ~ m]= w[x]
    for(int i=0;i<g[x].size();i++)
    {
        int y=g[x][i];
        dfs(y);//一路搜到底
        //以体积为第二维,必须包含x节点  
        for(int j=m;j>=v[x];j--)//j可选体积的范围为v[x]~m, 小于v[x]无法选择以x为子树的物品
        { //以子树所占体积分组
            for(int k=0;k<=j-v[x];k++)//分给子树y的空间不能大于j-v[x],不然都无法选根物品x
            {
                f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
            }
        }
    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int h;
        cin>>v[i]>>w[i]>>h;
        if(h==-1)
            root=i;
        else
            g[h].push_back(i);
    }
    dfs(root);
    cout<<f[root][m];
    return 0;
}
 

背包问题求方案数

求几种方案可以得到最优解。

我最开始简单的,用map去记录dp[j],最后直接输出mp[dp[j]]。这样会重复

这就要提到前面01背包所提到的赋值问题。

当dp全部初始化为0时,dp[m]的结果是体积小于等于m,如果体积为k,那么dp[k]就已经记录一次。

那我换成第二种赋值不就好了么?

不行!

还是会重复的!因为j一直在循环,在选择不同物品时,dp[j]都出现过。

代码注释中举例了,第二次看这个代码,竟然又忽略了这个问题,提交又错一发,这么浅显,一个坑掉两次。

上述可以看出,影响计算的就是体积,同种方案不同体积,同种体积同种方案都会重复。

那我们可以开个数组,用来存放某一体积下的方案数呀,这时候再判断一下该体积是否可以达到最优解,累加到一块就是答案

首先初始化mp[i]=0,

mp[0]=1 体积为零,都不选,为1种,依次开端,向后递推。为了确保一定是该体积下的结果。当向后不断考虑更多物品时,我们采用更新,而不是累加,因为最终我们是针对全部物品的不同体积的方案数。

代码

c++ 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define fir(i,a,b) for(int i=a;i<=b;i++)
#define PII pair<int,int> 
#define fi first
#define se second
#define tup tuple<int,int,int>
const int mod=1e9+7;
const int N=2010;
int dp[N], n,m,v[1100],w[1100];
map<int,int> mp;
signed main()
{
      
     cin>>n>>m;
     mp[0]=1;//某体积对应的方案数,体积为零,都不选,为1种
     fir(i,1,n)
     {
         cin>>v[i]>>w[i];
         for(int j=m;j>=v[i];j--)
         {
             int t=max(dp[j],dp[j-v[i]]+w[i]),c=0;
             if(t==dp[j])
             c+=mp[j];
             if(t==dp[j-v[i]]+w[i]) c+=mp[j-v[i]];
             dp[j]=t,mp[j]=c%mod;//不能直接用g[j]+=....,这里是根据不同体积进行的更新,+=会重复计算
           //  mp[dp[j]]= (mp[dp[j]]+1) ;会重复,不能看价值,因为可能考虑有前i个dp[5]=9,考虑前i+1个dp[5]=9,而第i+1个不选,这是一种,累加就多算了       
         } 
     }//dp[m]表示总体积<=m的最大价值,此时总体积可能不等于m,但是我们mp计算的就是体积为j的方案数,不会重复
     int ans=0;
    fir(i,0,m)
    {
        if(dp[i]==dp[m])
        ans=(ans+mp[i])%mod;
        
    }
     cout<<ans<<'\n';
}

疑点又来了,怎么计算才能使该体积下的方案数,就一定是等于该体积?

我们分析为什么dp[j]会出现这种

因为 d [ j ] 会从 d p [ j − v [ i ] ] + w [ i ] d[j]会从dp[j-v[i]]+w[i] d[j]会从dp[j−v[i]]+w[i]转移过来。dp[k]初始化为零,巧在 + w [ i ] +w[i] +w[i],让他成功转移。

而mp,我们是从 m p [ j ] 和 m p [ j − 1 ] mp[j]和mp[j-1] mp[j]和mp[j−1]直接转移更新,不会无中生有

背包问题求具体方案

输出最优解中所选物品的编号序列,且该编号序列的字典序最小。

既然字典序最小,那么编号较小的优先考虑装进去。

这里我们倒着遍历物品,然后再正序回溯判断,该物品是否选择。

因为我们是倒序递推的,所以判断在考虑 i + 1 i+1 i+1时的最优解 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j],以及在此基础上选择物品 i i i的价值 d p [ i + 1 ] [ j − v [ i ] ] + w [ i ] dp[i+1][j-v[i]]+w[i] dp[i+1][j−v[i]]+w[i]哪个更大 ,由此得出是否选择物品 i i i。

先看代码,至于为什么后面会说。

代码

c++ 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define fir(i,a,b) for(int i=a;i<=b;i++)
#define PII pair<int,int> 
#define fi first
#define se second
#define tup tuple<int,int,int>
int dp[1010][1010];
 int n,m,v[1010],w[1010];
signed main()
{
    IOS
    
    cin>>n>>m;
    fir(i,1,n)
    cin>>v[i]>>w[i];
    for(int i=n;i>=1;i--)
    {
        fir(j,0,m)
        {
            dp[i][j]=dp[i+1][j];//因为倒着来,这里是i+1!!!
            if(j>=v[i])
            dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
        }
    }
    int i=1,j=m;
    while(i<=n)
    {
        if(j>=v[i]&&dp[i+1][j-v[i]]+w[i]>=dp[i+1][j])
        {
             cout<<i<<' ';
             j-=v[i];
        }
        i++;
    }
}

为什么递推后,求解断具体方案,该物品取不取,需要逆向回溯去判断?

我理解的也不够透彻

以正序递推举例,递推时i是不断增大去更新的,我们需要的也是更新完n个的结果。所以最后更新的才能反应出最优解。

如果我们按照递推的顺序以 i − 1 i-1 i−1去判断是否选取 i i i,这时候只是得到基于考虑$前i个 $物品的最优解答案,这并不是最终的最有价值。

所以这时我们需要逆向考虑是否选取物品 i i i.
那为什么不这样,像原来的01背包去正向递推,然后再逆向判断该物品是否选择,再反转成字典序递增的顺序输出呢?

假如有两个物品4,9,选择任意一个效果都是一样的,但由于你是在倒着判断,先判断9是否选取,那你要不要选择呢?

选择的话,字典序不是最小了

不选择的话,你又不知道会遇到一个物品4和它效果一样。

这时就进退两难了。

那么把顺序反过来不就解决了没,倒着递推,正序判断,能选择就选,正好保证字典序最小。

相关推荐
苓诣18 小时前
不同路径
动态规划
nuyoah♂20 小时前
DAY36|动态规划Part04|LeetCode:1049. 最后一块石头的重量 II、494. 目标和、474.一和零
算法·leetcode·动态规划
Colinnian1 天前
Codeforces Round 994 (Div. 2)-D题
算法·动态规划
დ旧言~1 天前
专题八:背包问题
算法·leetcode·动态规划·推荐算法
林辞忧1 天前
动态规划<四> 回文串问题(含对应LeetcodeOJ题)
动态规划
闻缺陷则喜何志丹1 天前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
忘梓.1 天前
解锁动态规划的奥秘:从零到精通的创新思维解析(3)
算法·动态规划
Jessie_waverider4 天前
LeetCode刷题day29——动态规划(完全背包)
算法·leetcode·动态规划
忘梓.4 天前
解锁动态规划的奥秘:从零到精通的创新思维解析(2)
算法·动态规划
苓诣4 天前
分割等和子集
动态规划