动态规化之背包问题系列


1. 0-1背包

N件物品,容量为V的背包,每件物品有各自价值且只能被选择一次,求有限背包容量下装入物品总价值最大。

1.1 二维版本

1.1.1 思路

N: 物品数量
V: 背包容积
vi: 第i件物品的体积
wi:第i件物品的价值
i: 第i件物品
j : 背包容量

  1. 状态f[i][j]定义:前i个物品中,背包容量j下的最大价值。注意:j表示体积。 初始状态f[0][0] = 0
  • 当前状态依赖于之前状态,N件物品N次决策,每次对当前物品进行决策,f[i][j]由之前状态而来。
  1. 当前背包容量不够(j < v[i]),也就是当前物品选不了。前i个物品的最值为前i-1个的最值。f[i][j] = f[i - 1][j]
  2. 当前背包容量够,存在两种状态:选:f[i][j] = f[i - 1][j - v[i]] + w[i] 不选:f[i][j] = f[i - 1][j]
1.1.2 代码
#include <iostream>
#include <algorithm>

using namespace std;

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

int main() {
    cin >> n >> m;
    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 ++) { //从1开始遍历体积 
            if (j < v[i]) { // 当前背包容量不够
                f[i][j] = f[i - 1][j];
            } else { // 取选与不选两者中的最大值
                f[i][j] = max(f[i - 1][j - v[i]] + w[i], f[i - 1][j]);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

1.2 一维版本

1.2.1 思路

二维版本f[i][j]可以求得任意合法的i和j的最优解。只关注最终状态f[n][m],观察二维公式f[i][j] = f[i - 1][jxxxx],s所以可以采用一维数组。

  1. 初始状态定义改变:N件物品,背包容量j下的最优解。
  2. f[j] = max(f[j], f[i - 1][j - v[i]] + w[i]) 从小到大更新时,如果还是正序,则有f[较小体积]更新到f[较大体积],有可能本应该用第i -1轮的状态却用的是第i轮的状态。
  3. 例如:一维状态第i轮对体积为3的物品进行决策,f[7]由 f[4]得来 f[4]正确情况下应为f[i - 1][4], 正序时f[4]此时变成了f[i][4] (循环到第i轮时,f[j]是前i轮中已经决策的物品且背包容量j下的最大值),此时f[4]被污染,逆序时,f[4]还没有在第i轮计算,所以仍然是f[i - 1][4]
  4. 状态转移方程:f[j] = max(f[j], f[j - v[i]] + w[i])
1.2.2 代码
  • 优化一
#include <iostream>
#include <algorithm>

using namespace std;

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

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) 
        cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i ++) { //遍历背包数
        for (int j = m; j >= 0; j --) { //从1开始遍历体积 
            if (j < v[i]) { // 当前背包容量不够
                // f[i][j] = f[i - 1][j]; // 优化前
                f[j] = f[j];
            } else { // 取选与不选两者中的最大值
                // f[i][j] = max(f[i - 1][j - v[i]] + w[i], f[i - 1][j]); //优化前
                f[j] = max(f[j], f[j - v[i]] + w[i]);
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}
  1. 当背包容量大于v[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]);
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m; //n 物品数量 m 背包体积
int v[N], w[N]; //v 物品的体积 w 物品的价值
int f[N]; // 最大值

int main()
{
    cin >> n >> m;
    
    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 -- ) // 注意当背包容量大于v[i]时才会更新
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}


  1. 处理输入时,一个一个物品,一个一个体积,可以边输入边处理
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m; //n 物品数量 m 背包体积
int f[N]; // 最大值

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

2. 完全背包

N种物品,容量为V的背包,每种物品都有无限件可以使用。
第i种物品的体积是vi, 价值是wi。求哪些物品装入背包,可以使这些物品的总体积不超过背包容量,且总价值最大。

2.1 暴力做法

2.1.1 思路

时间复杂度 O ( n 3 ) 时间复杂度O(n^3) 时间复杂度O(n3)

  • f [ i ] [ j ] f[i][j] f[i][j]定义:前i个物品,背包容量j下的最优解。
  • 每一轮循环对第i个物品决策,选择多少个 ( 0 − ⌊ j / v ⌋ ) (0-\lfloor j/v\rfloor) 0j/v第i件物品。
  • 每次可选取多个重复物品。
2.1.2 代码
#include <iostream>

using namespace std;

const int N = 10010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    cin >> n >> m;
    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 ++) {
            int count = j / v[i];
            for (int k = 0; k <= count; k ++) {
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    cout << f[n][m] << endl;
    return 0;
}

2.2 优化做法

2.2.1 思路

时间复杂度 O ( n 2 ) 时间复杂度O(n^2) 时间复杂度O(n2)
上述暴力做法的状态转移方程:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w 1 , f [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w 1 , . . . , f [ i − 1 ] [ j − ⌊ j / v ⌋ ] ∗ v + ⌊ j / v ⌋ ] ∗ w ) f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w_1, f[i - 1][j - 2 * v] + 2 * w_1, ...,f[i - 1][j - \lfloor j/v \rfloor] * v + \lfloor j/v \rfloor] * w) f[i][j]=max(f[i1][j],f[i1][jv]+w1,f[i1][j2v]+2w1,...,f[i1][jj/v⌋]v+j/v⌋]w)考虑到计算前i个物品的j体积的最优解f[i][j],而前i - 1个物品的最优解f[i - 1][j]在上一轮循环中已经计算完毕,现在只需要判断选择几个第i个种物品得到的价值最大。变量替换一下,将j变成j - v,则有:
f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 ∗ v ] + w 1 , f [ i − 1 ] [ j − 3 ∗ v ] + 2 ∗ w 1 , . . . , f [ i − 1 ] [ j − ⌊ ( j − v ) / v ⌋ ] ∗ v + ⌊ ( j − v ) / v ⌋ ] ∗ w ) f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2 * v] + w_1, f[i - 1][j - 3 * v] + 2 * w_1, ...,f[i - 1][j - \lfloor (j - v)/v \rfloor] * v + \lfloor (j - v)/v \rfloor] * w) f[i][jv]=max(f[i1][jv],f[i1][j2v]+w1,f[i1][j3v]+2w1,...,f[i1][j⌊(jv)/v⌋]v+⌊(jv)/v⌋]w)
上述两个方程合并可以得到最新的状态转移方程:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v ] + w ) f[i][j] = max(f[i - 1][j], f[i][j - v] + w) f[i][j]=max(f[i1][j],f[i][jv]+w)
这样优化掉了k这层循环。

  • 不用计算每个循环第i个物品个数
  • f[i][j]定义:前i个物品j体积的最优解。
  • 状态方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v ] + w ) f[i][j] = max(f[i - 1][j], f[i][j - v] + w) f[i][j]=max(f[i1][j],f[i][jv]+w)
2.2.2 代码
#include <iostream>

using namespace std;

const int N = 10010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++)
        cin >> v[i] >> w[i];
    f[0][0] = 0;
    for (int i = 1; i <= n; i ++) 
        for (int j = 0; j <= m; j ++) {
            if (j >= v[i])
                f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]); // 选第i个物品
            else 
                f[i][j] = f[i - 1][j]; // 背包容量不够,第i个物品选不了
        }
    cout << f[n][m] << endl;
    return 0;
}

2.3 一维版本

2.3.1 代码一
#include <iostream>

using namespace std;

const int N = 10010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++)
        cin >> v[i] >> w[i];
    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] << endl;
    return 0;
}

2.3.2 代码二
#include <iostream>

using namespace std;

const int N = 10010;

int n, m;
int f[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int v, w;
        cin >> v >> w;
        for (int j = v; j <= m; j ++) 
           f[j] = max(f[j], f[j - v] + w);
    }
    cout << f[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]); // 完全背包问题

3. 多重背包I

N种物品 容量为V的背包。
第i种物品最多s件, 每件体积是v,价值是w。
求哪些物品装入背包,可以使得物品体积总和不超过背包容量,且价值总和最大。

3.1 思路一

  • 与0-1背包类似,加上限定条件,枚举物品件数即可。

3.2 代码

#include <iostream>

using namespace std;

const int N = 110;
int n, m;
int v[N], w[N], s[N]; //第i种物品的体积 价值 数量 
int f[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) 
        cin >> v[i] >> w[i] >> s[i];
    for (int i = 1; i <= n; i ++) {
        for (int j = m; j >= 0; j -- )
            for (int k = 1; k <= s[i]; k ++) {
                if (j >= k * v[i])
                    f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
            }  
    }
    cout << f[m] << endl;
    return 0;
}

3.3 思路二

拆解。当si = 1时,相当于01背包中的一件物品;当si > 1时,相当于01背包中的多个一件物品。

3.4 代码

#include <bits/stdc++.h>

using namespace std;

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

int main() {
    cin >> n >> m;
    while (n --) {
        cin >> v >> w >> s;
        for (int i = 1; i <= s; i ++) {
            for (int j = m; j >= v; j --) {
                f[j] = max(f[j], f[j - v] + w);
            }
        }
    }
    
    cout << f[m] << endl;
    return 0;
}

4. 多重背包II

描述同上,数据范围扩大。考察多重背包的二进制优化方法。

数据范围
0 < N ≤ 1000 0 < N \leq 1000 0<N1000
0 < V ≤ 2000 0 < V \leq 2000 0<V2000
0 < v i , w i , s i ≤ 2000 0 < v_i, w_i, s_i \leq 2000 0<vi,wi,si2000

4.1 思路

任意数s,至少选多少个数可以表示成s内的所有数。
比如10, 就可以用1,2,4,3表示任意10以内的数。

a n s = ⌊ l o g 2 S ⌋ ans = \lfloor log_2S \rfloor ans=log2S
所以每个物品可以考虑拆成log(s)份,复杂度降低。转换为0-1背包问题。

4.2 代码

#include <bits/stdc++.h>

using namespace std;

const int N = 2010;
int n, m;
int f[N];

struct Good {
    int v, w;    
};

int main() {
    cin >> n >> m;
    vector<Good> goods;
    for (int i = 0; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        // 拆成log(s) 份
        for (int j = 1; j <= s; j *= 2) {
            s -= j;
            goods.push_back({v * j, w * j});
        }
        if (s > 0) {  // 如果还有剩余
            goods.push_back({v * s, w * s});
        }
    }
    
    for (auto good : goods) {
        for (int j = m; j >= good.v; j --) {
            f[j] = max(f[j], f[j - good.v] + good.w);
        }
    } 
    
    cout << f[m] << endl;
    return 0;
}

5. 分组背包

N组物品和一个容量为V的背包。
每组物品若干个,同一组内的物品最多只能选一个。
每件物品的体积是Vij, 价值是Wij,其中i是组号,j是组内编号。
将哪些物品装入背包,可使得物品总体积不超过背包容量,且总价值最大。

5.1 思路

0-1背包问题的变形。每组物品s个,每组物品的决策有s+1种选择(0~s种)。枚举这些决策即可。

5.2 代码

#include <bits/stdc++.h>

using namespace std;

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

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i ++) {
        int s;
        cin >> s;
        for (int j = 0; j < s; j ++) {
            cin >> v[j] >> w[j];
        }
        for (int j = m; j >= 0; 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] << endl;
    return 0;
}

参考

acwing 动态规划之背包问题系列

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值