动态规划-背包问题-背包九讲


title: 动态规划-背包问题
date: 2023-05-12 13:01:31
categories:

  • Algorithm
  • 动态规划
    tags:
  • 动态规划

01背包问题

题目

题目链接:https://www.acwing.com/problem/content/description/2/

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

i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N N N 行,每行两个整数 v i , w i v_i, w_i vi,wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0 \lt N, V \le 1000 0<N,V1000

0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

思路 O ( n 2 ) O(n^2) O(n2)

  • dp[i][j]表示前i个物品,背包容量为j下的最大价值

    • N件物品,需要进行N次决策,每一次对第i件物品进行决策
  • 当背包容量不够时j<v[i],前i个物品最大价值即为前i-1个物品的最大价值

    • dp[i][j]=dp[i-1][j]
  • 当背包容量够的时候,分两种情况,选与不选第i个物品,两种情况取Max

    • i:dp[i][j]=dp[i-1][j-v[i]]+w[i]
    • 不选i:dp[i][j]=dp[i-1][j]

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1), w(n + 1);
    vector<vector<int> > f(n + 1, vector<int>(m + 1));
    //所有从前i个物品中选,容量不超过j的最大价值
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            f[i][j]=f[i-1][j];
            if (j>=v[i]) f[i][j]= max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

思路-空间优化为 O ( n ) O(n) O(n)

维优化:

  • 将状态dp[i][j]变成一维dp[j],只需要做一个等价变形
  • dp[j]的含义:N件物品,背包容量为j下的最大价值
    • 注意枚举背包容量的时候必须从最大容量m开始枚举
  • 为什么需要逆序枚举:
    • 二维情况下,dp[i][j]是由上一轮i-1的状态得来的,dp[i][j]dp[i-1][j]互不影响
    • 一维情况下,如果还是用正序枚举,那么dp[较小体积]更新得到dp[较大体积],则有可能本该用第i-1轮的状态,却是用的第i轮的状态

如果j是顺序循环,那么f[j-w[i]]会先于f[j]更新,也就是说,会用新值f[j-w[i]]去更新f[j],所以出错

  • 状态转移方程:dp[j]=max(dp[j],dp[j-v[i]+w[i])
  • 我们的代码中决策到第 i件物品(循环到第i轮),f[j]就是前i轮已经决策的物品且背包容量 j 下的最大价值。

具体过程如下:

  当还未进入循环时:
  f[0] = 0;  f[1] = 0;  f[2] = 0;  f[3] = 0;  f[4] = 0;  
  f[5] = 0;  f[6] = 0;  f[7] = 0;  f[8] = 0;  f[9] = 0; f[10] = 0;
  当进入循环 i == 1 时:w[i] = 5; v[i] = 4;
  j = 10:f[10] = max(f[10], f[6] + 5);max(0, 5) = 5; 即f[10] = 5;
  j = 9 :f[9] = max(f[9], f[5] + 5);max(0, 5) = 5; 即f[9] = 5;
  j = 8 :f[8] = max(f[8], f[4] + 5);max(0, 5) = 5; 即f[8] = 5;
  j = 7 :f[7] = max(f[7], f[3] + 5);max(0, 5) = 5; 即f[7] = 5;
  j = 6 :f[6] = max(f[6], f[2] + 5);max(0, 5) = 5; 即f[6] = 5;
  j = 5 :f[5] = max(f[5], f[1] + 5);max(0, 5) = 5; 即f[5] = 5;
  j = 4 :f[6] = max(f[4], f[0] + 5);max(0, 5) = 5; 即f[4] = 5;
  当进入循环 i == 2 时:w[i] = 6; v[i] = 5; 
  j = 10:f[10] = max(f[10], f[5] + 6);max(5, 11) = 11; 即f[10] = 11;
  j = 9 :f[9] = max(f[9], f[4] + 6);max(5, 11) = 5; 即f[9] = 11;
  j = 8 :f[8] = max(f[8], f[3] + 6);max(5, 6) = 6; 即f[8] = 6;
  j = 7 :f[7] = max(f[7], f[2] + 6);max(5, 6) = 6; 即f[7] = 6;
  j = 6 :f[6] = max(f[6], f[1] + 6);max(5, 6) = 6; 即f[6] = 6;
  j = 5 :f[5] = max(f[5], f[0] + 6);max(5, 6) = 6; 即f[5] = 6;
  当进入循环 i == 3: w[i] = 7; v[i] = 6; 
  j = 10:f[10] = max(f[10], f[4] + 7);max(11, 12) = 12; 即f[10] = 12;
  j = 9 :f[9] = max(f[9], f[3] + 6);max(11, 6) = 11; 即f[9] = 11;
  j = 8 :f[8] = max(f[8], f[2] + 6);max(6, 6) = 6; 即f[8] = 6;
  j = 7 :f[7] = max(f[7], f[1] + 6);max(6, 6) = 6; 即f[7] = 6;
  j = 6 :f[6] = max(f[6], f[0] + 6);max(6, 6) = 6; 即f[6] = 6;
数据来源:https://www.acwing.com/solution/content/30250/

代码

#include <bits/stdc++.h>


using namespace std;

int main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1), w(n + 1);
    vector<int> f(m + 1);
    //所有从前i个物品中选,容量不超过j的最大价值
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= v[i]; j--) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

完全背包问题

题目

链接:https://www.acwing.com/problem/content/3/

N N N 种物品和一个容量是 V V V 的背包,每种物品都有无限件可用。

i i i 种物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品种数和背包容积。

接下来有 N N N 行,每行两个整数 v i , w i v_i, w_i vi,wi,用空格隔开,分别表示第 i i i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0 \lt N, V \le 1000 0<N,V1000
0 < v i , w i ≤ 1000 0 \lt v_i, w_i \le 1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10

思路(朴素做法)

f[i][j]表示将前i件物品放进容量为j的背包的最大价值

  • 如果当前背包容量j<w[i],不能放入,则f[i][j]=f[i-1][j]
  • 如果当前背包容量j>=w[i],可以放入:
    • 若第i件物品不放入,则f[i][j]=f[i-1][j]
    • 若第i件物品放入,则f[i][j]=f[i][j-w[i]]+c[i]

状态转移方程:

  • f[i][j]=f[i-1][j],(j<w[i])
  • f[i][j]=max(f[i-1][j],f[i][j-w[i]]+v[i]),(j>=w[i])

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1), w(n + 1);
    vector<vector<int> > f(n + 1, vector<int>(m + 1));
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (j < v[i]) f[i][j] = f[i - 1][j];
                else f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
            }
        }
    cout << f[n][m] << endl;

    return 0;
}

优化(一维空间)

可以将空间优化掉一维,优化之后从前往后枚举即可,区别于01背包:

  • f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

  • f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1), w(n + 1);
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    vector<int> f(m + 1);
    for (int i = 1; i <= n; i++) {
        for (int j = v[i]; j <= m; j++) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];

    return 0;
}

多重背包问题

题目

https://www.acwing.com/problem/content/description/4/

N N N 种物品和一个容量是 V V V 的背包。

i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品种数和背包容积。

接下来有 N N N 行,每行三个整数 v i , w i , s i v_i, w_i, s_i vi,wi,si,用空格隔开,分别表示第 i i i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

见不同思路

输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10

思路(朴素 O ( n ∗ m ∗ ∑ s i ) O(n*m*\sum{s_i}) O(nmsi)

数据范围为:

0 < N , V ≤ 100 0 \lt N, V \le 100 0<N,V100
0 < v i , w i , s i ≤ 100 0 \lt v_i, w_i, s_i \le 100 0<vi,wi,si100

dp[i][j]表示从前i个物品中选,总体积不超过j的最大价值

根据第i个物品选多少个进行划分,假设选k个,则dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]*k]+w[i]*k)

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1), w(n + 1), s(n + 1);
    vector<vector<int> > f(n + 1, vector<int>(m + 1));
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    }
    cout << f[n][m];
    return 0;
}

思路(二进制 O ( n ∗ m ∗ ∑ l o g s i ) O(n*m*\sum{log{s_i}}) O(nmlogsi))

  • 二进制优化,举例说明,假设有11个商品

    • 正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。
    • 现在用二进制优化,把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “,
    • 这样将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。

二进制拆分思想:将第i种物品拆分为若干件物品,每件物品的体积和价值乘以一个拆分系数( 1 , 2 1 , 2 2 , . . . 2 k − 1 , s i − 2 k + 1 1,2^1,2^2,...2^{k-1},s_i-2^k+1 1,21,22,...2k1,si2k+1)

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> a, b;
    for (int i = 1; i <= n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = 1; j <= s; j <<= 1) { //二进制拆分
            a.push_back(j * v);
            b.push_back(j * w);
            s -= j;
        }
        if (s){ //还剩下物品
            a.push_back(s*v);
            b.push_back(s*w);
        }
    }
    //01背包
    vector<int> f(m+1);
    for(int i=0;i<a.size();i++){
        for(int j=m;j>=a[i];j--){
            f[j]= max(f[j],f[j-a[i]]+b[i]);
        }
    }
    cout<<f[m];
    return 0;
}

思路(单调队列 O ( n ∗ m ) O(n*m) O(nm))

规律1:

f数组是按类进行更新的,可以把f[0...m]按体积v的余数拆分为v个类。

  • f[0],f[v],f[2v],...f[kv]
  • f[1],f[1+v],...f[1+kv];
  • f[j],f[j+v],...f[j+kv];

其中jv的余数, 0 < = j < = v − 1 0<=j<=v-1 0<=j<=v1,例如

  • f[0],f[2],f[4],f[6],f[8]
  • f[1],f[3],f[5],f[7],f[9]

规律2:

f[j]是由前面不超过数量s的同类值传递推得到的,这就相当于从前面宽度为s的窗口中挑选最大值来更新当前的值,因此,我们可以使用单调队列来维护窗口的最大值,从而将更新f[j]的次数降低为1次,这时候需要顺序更新f值,需要增加一个备份数组。

改造后如下顺序循环

  int n, m;
  cin >> n >> m;
  vector<int> v(n + 1), w(n + 1), s(n + 1);
  vector<int> f(m + 1), g(m + 1);
  for (int i = 1; i <= n; i++) {
      std::memcpy(g, f, sizeof f);
      for (int j = 1; j <= m; j++) {
          for (int k = 1; k <= s[i] && v[i] * s[i] <= j; k++)
              f[j]= max(g[j],g[j-k*v[i]]+k*w[i]);
      }
  }

在这里插入图片描述

每次去使用队头更新当前的最大值,但是注意队头不一定就是整个队列中的最大值,队头+还可以放入物品的价值是整个队列中最大的

  • h表示队头,t表示队尾,变量j表示类 j ∈ [ 0 , v ) j\in[0,v) j[0,v),k表示背包容量
  • 维护的窗口为[k-sv,k-v]
  • 当前还可以放入物品的个数为(k-q[h]/v)
  • 如果用g[k]比用g[q[t]]更新f[x]获得更大价值,可以有g[k]+(x-k)/v*w>=g[q[t]]+(x-q[t])/v*w,即g[k]>=g[q[t]]+(k-q[t])/v*w,那么就将队尾出队,表明g[k]g[q[t]]更有价值

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 2e4 + 10;
int f[N], g[N], q[N];//q数组存下标


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m, v, w, s;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        std::memcpy(g, f, sizeof f);
        cin >> v >> w >> s;
        for (int j = 0; j < v; j++) {
            int h = 0, t = -1;//h为头,t为尾
            for (int k = j; k <= m; k += v) {
                //q[h]不在窗口[k-s*v,k-v]中,队头出队
                if (h <= t && q[h] < k - s * v) h++;
                //使用队头最大值来更新f (k-q[h])/v是还可以放进物品的个数
                if (h <= t) f[k] = max(g[k], g[q[h]] + (k - q[h]) / v * w);
                //当前值比队尾更有价值,队尾出队
                while (h <= t && g[k] >= g[q[t]] + (k - q[t]) / v * w) t--;
                //下标入队,便于队头出队
                q[++t] = k;
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

混合背包问题

题目

链接:https://www.acwing.com/problem/content/7/

N N N 种物品和一个容量是 V V V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 s i s_i si 次(多重背包);

每种体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品种数和背包容积。

接下来有 N N N 行,每行三个整数 v i , w i , s i v_i, w_i, s_i vi,wi,si,用空格隔开,分别表示第 i i i i i i 种物品的体积、价值和数量。

  • s i = − 1 s_i = -1 si=1 表示第 i i i 种物品只能用1次;
  • s i = 0 s_i = 0 si=0 表示第 i i i 种物品可以用无限次;
  • s i > 0 s_i >0 si>0 表示第 i i i 种物品可以使用 s i s_i si 次;
输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0 \lt N, V \le 1000 0<N,V1000
0 < v i , w i ≤ 1000 0 \lt v_i, w_i \le 1000 0<vi,wi1000
− 1 ≤ s i ≤ 1000 -1 \le s_i \le 1000 1si1000

输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8

思路

参考上面的01背包问题,完全背包问题,多重背包问题

可以将多重背包问题用二进制优化的思路转换为01背包问题,这样就只需要做01背包和完全背包问题了,

  • f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

  • f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

优化为1维之后:f[j]=max(f[j],f[j-v]+w)

  • 01背包:从 m − > v m->v m>v枚举
  • 完全背包:从 v − > m v->m v>m枚举

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<int> f(m + 1);
    for (int i = 1; i <= n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        if (s == 0) {//完全背包问题
            for (int j = v; j <= m; j++)
                f[j] = max(f[j], f[j - v] + w);
        } else {//多重背包问题和01背包问题
            if (s == -1) s = 1;
            for (int j = 1; j <= s; j <<= 1) {
                for (int k = m; k >= j*v; k--) {
                    f[k]= max(f[k],f[k-j*v]+j*w);
                }
                s-=j;
            }
            if (s){
                for(int k=m;k>=s*v;k--){
                    f[k]= max(f[k],f[k-s*v]+s*w);
                }
            }
        }
    }
    cout<<f[m]<<endl;

    return 0;
}

二维费用的背包问题

题目

链接:https://www.acwing.com/problem/content/8/

N N N 件物品和一个容量是 V V V 的背包,背包能承受的最大重量是 M M M

每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值是 w i w_i wi

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

输入格式

第一行三个整数, N , V , M N,V, M N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N N N 行,每行三个整数 v i , m i , w i v_i, m_i, w_i vi,mi,wi,用空格隔开,分别表示第 i i i 件物品的体积、重量和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N ≤ 1000 0 \lt N \le 1000 0<N1000
0 < V , M ≤ 100 0 \lt V, M \le 100 0<V,M100
0 < v i , m i ≤ 100 0 \lt v_i, m_i \le 100 0<vi,mi100
0 < w i ≤ 1000 0 \lt w_i \le 1000 0<wi1000

输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8

思路

动态规划:

  • 状态表示:f[i,j,k]所有从前i个物品中选,并且总体积不超过j,总重量不超过k的选法的最大价值
  • 状态计算:f[i,j,k]=max(f[i-1,j,k],f[i-1,j-v1[i],k-v2[i]]+w[i])

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 110;
int f[N][N];


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m, w;//个数,体积,容量
    cin >> n >> m >> w;
    for (int i = 1; i <= n; i++) {
        int a, b, c;//体积,重量,价值
        cin >> a >> b >> c;
        for(int j=m;j>=a;j--){
            for(int k=w;k>=b;k--){
                f[j][k]= max(f[j][k],f[j-a][k-b]+c);
            }
        }
    }
    cout<<f[m][w];


    return 0;
}

分组背包问题

题目

链接:https://www.acwing.com/problem/content/9/

N N N 组物品和一个容量是 V V V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i i i 是组号, j j j 是组内编号。

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

输出最大价值。

输入格式

第一行有两个整数 N , V N,V NV,用空格隔开,分别表示物品组数和背包容量。

接下来有 N N N 组数据:

  • 每组数据第一行有一个整数 S i S_i Si i i i个物品组的物品数量;
  • 每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j , w i j v_{ij}, w_{ij} vij,wij,用空格隔开,分别表示第 i i i 个物品组的第 j j j 个物品的体积和价值;
输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 100 0 \lt N, V \le 100 0<N,V100
0 < S i ≤ 100 0 \lt S_i \le 100 0<Si100
0 < v i j , w i j ≤ 100 0 \lt v_{ij}, w_{ij} \le 100 0<vij,wij100

输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8

思路(朴素 O ( n ∗ v ∗ ∑ s i ) O(n*v*\sum{s_i}) O(nvsi))

状态表示:f[i][j]表示前i组物品,能放入容量为j的背包的最大价值

循环物品组,循环背包容量,对于第i组物品,容量为j的背包,有s+1种选法,取最大值

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 110;
int v[N][N], w[N][N], s[N];
int f[N][N];
int n, m;

signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        for (int j = 1; j <= s[i]; j++) {
            cin >> v[i][j] >> w[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=0;k<=s[i];k++){
                if (j>=v[i][k]){
                    f[i][j]= max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
                }
            }
        }
    }
    cout<<f[n][m]<<endl;

    return 0;
}

思路(空间优化1维)

  • dp[j]表示总体积为j时的最大价值
  • 必须先枚举体积,再枚举每一个物品。因为如果先枚举物品,那么你下一个物品不管从哪里转移都已经被上一个更新,
  • 同时,体积必须倒着枚举,否则一个物品将会被重复选择。

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 110;
int v[N], w[N];
int f[N];
int n, m, s;

signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s;
        for (int j = 1; j <= s; j++) cin >> v[j] >> w[j];
        for (int j = m; j >= 1; j--) {
            for(int k=0;k<=s;k++){
                if (j>=v[k]){
                    f[j]= max(f[j],f[j-v[k]]+w[k]);
                }
            }
        }
    }
    cout<<f[m];

    return 0;
}

有依赖的背包问题

题目

链接:https://www.acwing.com/problem/content/10/

N N N 个物品和一个容量是 V V V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i i i,体积是 v i v_i vi,价值是 w i w_i wi,依赖的父节点编号是 p i p_i pi。物品的下标范围是 1 … N 1 … N 1N

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

输出最大价值。

输入格式

第一行有两个整数 N , V N,V NV,用空格隔开,分别表示物品个数和背包容量。

接下来有 N N N 行数据,每行数据表示一个物品。
i i i 行有三个整数 v i , w i , p i v_i, w_i, p_i vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 p i = − 1 p_i = -1 pi=1,表示根节点。 数据保证所有物品构成一棵树。

输出格式

输出一个整数,表示最大价值。

数据范围

1 ≤ N , V ≤ 100 1 \le N, V \le 100 1N,V100
1 ≤ v i , w i ≤ 100 1 \le v_i, w_i\le 100 1vi,wi100

父节点编号范围:

  • 内部结点: 1 ≤ p i ≤ N 1 \le p_i \le N 1piN;
  • 根节点 p i = − 1 p_i = -1 pi=1;
输入样例
5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2
输出样例:
11

思路

考虑以u为根节点的子树的所有物品,这颗子树的物品都最大价值应该是节点u和背包容量j的函数

f[u][j]表示选择以u为子树的物品,在体积和不超过j时的最大价值

每个节点有i个节点 s 1 , s 2 . . . s i s_1,s_2...s_i s1,s2...si可以选或者不选,可以将u的i个子节点看成i组物品,每组物品 s i s_i si按体积拆分,有 0 , 1 , 2 , . . . j − v [ i ] 0,1,2,...j-v[i] 0,1,2,...jv[i]种决策

按单位体积拆分是因为 s i s_i si的子孙有可能存在体积为1的物品,

拆分到 j − v [ u ] j-v[u] jv[u]是因为要留出 v [ u ] v[u] v[u]到空间装入节点u的物品

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 110;
int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u) {
    //f[u][i]表示以u为子树,体积不超过i的最大价值
    for (int i = v[u]; i <= m; i++) f[u][i] = w[u];
    for (int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        dfs(to);
        for (int j = m; j >= v[u]; j--) {
            for (int k = 0; k <= j - v[u]; k++) {
                f[u][j] = max(f[u][j], f[u][j - k] + f[to][k]);
            }
        }
    }
}


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    cin >> n >> m;
    memset(h, -1, sizeof h);
    int root;
    for (int i = 1; i <= n; i++) {
        int p;//每个节点的父节点
        cin >> v[i] >> w[i] >> p;
        if (p == -1) root = i;
        else add(p, i);
    }
    dfs(root);
    cout << f[root][m];
    return 0;
}

背包问题求方案数

题目

链接:https://www.acwing.com/problem/content/11/

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

i i i i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输出 最优选法的方案数。注意答案可能很大,请输出答案模 1 0 9 + 7 10^9 + 7 109+7 的结果。

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N N N 行,每行两个整数 v i , w i v_i, w_i vi,wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。

输出格式

输出一个整数,表示 方案数 1 0 9 + 7 10^9 + 7 109+7 的结果。

数据范围

0 < N , V ≤ 1000 0 \lt N, V \le 1000 0<N,V1000
0 < v i , w i ≤ 1000 0\lt v_i, w_i \le 1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
2

思路

f[i]表示背包容量为i时能装入物品的最大价值

c[i]表示背包容量为i时最优选法的方案数

容量从j-v增加到j,只是增加了一件物品,方案数没有改变c[j]=c[j-v]

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 1010, mod = 1e9 + 7;
int n, m;

signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    cin >> n >> m;
    vector<int> f(m + 1), c(m + 1, 1);//不选也是一种方案
    for (int i = 1; i <= n; i++) {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; j--) {
            if (f[j - v] + w > f[j]) {//
                f[j] = f[j - v] + w;
                c[j] = c[j - v];//只是增加一个物品,方案数不变
            } else if (f[j - v] + w == f[j]) {//说明有新的选法
                c[j] = (c[j] + c[j - v]) % mod;
            }
        }
    }
    cout<<c[m]<<endl;


    return 0;
}

背包问题求具体方案

题目

链接:https://www.acwing.com/problem/content/12/

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

i i i i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1 … N 1 … N 1N

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N N N 行,每行两个整数 v i , w i v_i, w_i vi,wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。

输出格式

输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1 … N 1 … N 1N

数据范围

0 < N , V ≤ 1000 0 \lt N, V \le 1000 0<N,V1000
0 < v i , w i ≤ 1000 0\lt v_i, w_i \le 1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
1 4

思路

状态表示:f[i][j]表示从第i个物品到最后一个物品装入容量为j的背包的最优解

与之前的区别就是这里是倒着的

选第i个:f[i][j]=f[i+1][j]

不选:f[i][j]=f[i+1][j-v[i]]+w[i]

计算完状态后:f[1][m]就是最大价值,从f[1][m]开始搜索

对于f[i][j]得到的方式,确定是否选择第i个物品:

  • f[i][j]=f[i+1][j],不选第i个物品
  • f[i][j]=f[i+1][j-v[i]]+w[i],选第i个物品
  • f[i][j]=f[i+1][j]=f[i+1][j-v[i]]+w[i],都可以得到最优解,为了字典序最小,必须选。

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;


signed main() {
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    int n, m;
    cin >> n >> m;
    vector<vector<int> > f(n + 2, vector<int>(m + 1));
    vector<int> v(n + 2), w(n + 2);
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i = n; i >= 1; i--) {
        for (int j = 0; j <= m; j++) {
            f[i][j] = f[i + 1][j];
            if (j >= v[i])
                f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    }

    int j = m;
    for (int i = 1; i <= n; i++) {
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
            cout << i << " ";
            j -= v[i];
        }
    }


    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重生之我是cxk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值