动态规划:背包问题

108 阅读4分钟

背包问题

背包问题是一类经典的可以应用动态规划来解决的问题。背包问题的基本描述如下:给定一组物品,每种物品都有其重量和价格,在限定的总重量内如何选择才能使物品的总价格最高。由于问题是关于如何选择最合适的物品放置于给定的背包中,因此这类问题通常被称为背包问题。根据物品的特点,背包问题还可以进一步细分。

  • 如果每种物品只有一个,可以选择将之放入或不放入背包,那么可以将这类问题称为0-1背包问题。0-1背包问题是最基本的背包问题,其他背包问题通常可以转化为0-1背包问题。
  • 如果第i种物品最多有MiM_i个,也就是每种物品的数量都是有限的,那么这类背包问题称为有界背包问题(也可以称为多重背包问题)。
  • 如果每种物品的数量都是无限的,那么这类背包问题称为无界背包问题(也可以称为完全背包问题)

0-1背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次.第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值

dp[i][j]表示只考虑前i个物品,总体积不超过j的最大价值。状态转移方程:dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i])对于j的遍历,从大到小进行更新,可以进行空间压缩到一维dp[j]dp[j]=max(dp[j],dp[jv[i]]+w[i])\begin{array}{l} dp[i][j]表示只考虑前i个物品,总体积不超过j的最大价值。\\ 状态转移方程:\\ dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]) \\ \\ 对于j的遍历,从大到小进行更新,可以进行空间压缩到一维dp[j] \\ dp[j] = max(dp[j], dp[j-v[i]] + w[i]) \end{array}
// n为物品数量, m为背包体积
for (int i = 0; i < n; i++) {
    for (int j = m; j >= 0 && j - v[i] >= 0; j--) {
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    }
}
//最终结果为dp[m];

最原始的01背包问题,可以参见ACwing的01背包问题,代码实现

#include <iostream>
#include <vector>
using namespace std;

const int N = 1010;
int v[N], w[N];
int dp[N];

int main() {
    // 获取输入
    int n, m; // 分别表示物品数量和背包体积
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        cin >> v[i] >> w[i];
    }
    
    // 动态规划主体
    for (int i = 0; i < n; i++) {
        for (int j = m; j >= v[i]; j--) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

练习题

在《剑指Offer专项突破版》也给出了两道相关的练习题。

剑指 Offer II 101. 分割等和子集,假设数组中所有元素的和为sum,该问题可以转化为:如果sum是奇数,肯定不存在想要的结果;如果sum是偶数,那么问题转化为是否可以在数组中查找到一些数的和为sum2\frac{sum}{2},问题转化成了0-1背包问题。

bool canPartition(vector<int>& nums) {
    int sum = 0;
    for (int i = 0; i < nums.size(); i++) sum += nums[i];
    if (sum & 1) return false;

    int target = sum >> 1;
    vector<bool> dp(target+1);
    dp[0] = true;
    for (int i = 0; i < nums.size(); i++) {
        for (int j = target; j >= nums[i]; j--) {
            dp[j] = dp[j] | dp[j-nums[i]];
        }
    }
    return dp[target];
}

剑指 Offer II 102. 加减的目标值

+号的数组子集为A,号的数组子集为B,数组中所有元素的和为sum,则有AB=sumA+B=targetB=sumtarget2\begin{array}{l} 记+号的数组子集为A,添-号的数组子集为B,数组中所有元素的和为sum, 则有\\ A - B = sum \\ A + B = target \\ \\ B = \frac{sum-target}{2} \end{array}

因而上述问题转化为如果target > sum 或者sum-target为奇数则不存在符合条件的数组,否则,在数组中寻找和为sumtarget2\frac{sum-target}{2}的组合数目,问题转化为0-1背包问题。

int findTargetSumWays(vector<int>& nums, int target) {
    int sum = 0;
    for (int i = 0; i < nums.size(); i++) sum += nums[i];
    target = (sum - target);
    if (target < 0 || target & 1) return 0;

    target = target >> 1;
    vector<int> dp(target+1);
    dp[0] = 1;
    for (int i = 0; i < nums.size(); i++) {
        for (int j = target; j >= nums[i]; j--) {
            dp[j] += dp[j-nums[i]];
        }
    }
    return dp[target];
}

完全背包问题

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

状态转移方程:i个物品可被选择的数量为0,1,2......,k...dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i],dp[i1][j2v[i]]+2w[i],.....,dp[i1][jkv[i]]+kw[i],......)dp[i1][jv[i]]=max(dp[i1][jv[i]],dp[i1][j2v[i]]+w[i],dp[i1][j3v[i]]+2w[i],.....,dp[i1][j(k+1)v[i]]+kw[i],......)可以得出:dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i])01背包类似,将j0m遍历,可以进行空间压缩dp[j]=max(dp[j],dp[jv[i]]+w[i])\begin{array}{l} 状态转移方程:\\ 第i个物品可被选择的数量为0, 1, 2 ......, k ...\\ dp[i][j] = max(dp[i-1][j], \\ dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]] + 2*w[i], ....., dp[i-1][j-k*v[i]] + k*w[i], ......)\\ \\ dp[i-1][j-v[i]] = max(\\ dp[i-1][j-v[i]], dp[i-1][j-2*v[i]]+w[i], dp[i-1][j-3*v[i]] + 2*w[i], ....., dp[i-1][j-(k+1)*v[i]] + k*w[i], ......)\\ \\ \\ 可以得出:\\ dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]) \\ \\ 和01背包类似,将j从0到m遍历,可以进行空间压缩\\ dp[j] = max(dp[j], dp[j-v[i]]+w[i]) \end{array}
// n为物品数量, m为背包体积
for (int i = 0; i < n; i++) {
    for (int j = v[i]; j <= m; j++) {
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    }
}
// 最终结果为dp[m];

最原始的完全背包问题,参见完全背包,代码实现:

#include <iostream>
#include <vector>
using namespace std;

const int N = 1010;
int v[N], w[N];
int dp[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        cin >> v[i] >> w[i];
    }

    // 动态规划实现主体
    for (int i = 0; i < n; i++) {
        for (int j = v[i]; j <= m; j++) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

练习题

在《剑指Offer专项突破版》也给出了一道相关的练习题

剑指 Offer II 103. 最少的硬币数目

int coinChange(vector<int>& coins, int amount) {
    if (amount < 0) return -1;
    vector<int> dp(amount+1, INT_MAX);
    dp[0] = 0;
    for (int i = 0; i < coins.size(); i++) {
        for (int j = coins[i]; j <= amount; j++) {
            if (dp[j-coins[i]] != INT_MAX)
                dp[j] = min(dp[j], dp[j-coins[i]]+1);
        }
    }
    return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}

多重背包问题

有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

dp[i][j]=max(dp[i1][j],dp[i1][jvi]+w[i],dp[i1][j2vi]+2w[i],.....,dp[i1][jkvi]+kw[i])其中,k<=s[i]jkvi>=0\begin{array}{l} dp[i][j] = max(dp[i-1][j], dp[i-1][j-v_i]+w[i], dp[i-1][j-2*v_i] + 2*w[i], ....., dp[i-1][j-k*v_i] + k*w[i]) \\ 其中,k <= s[i]且j - k * v_i >= 0 \end{array}

多种背包问题

#include <iostream>
#include <vector>
using namespace std;

const int N = 110;
int v[N], w[N], s[N];
int dp[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        cin >> v[i] >> w[i] >> s[i];
    }

    // 动态规划实现主体
    for (int i = 0; i < n; i++) {
        for (int j = m; j >= 0; j--) {
            for (int k = 1; k <= s[i] && j - k * v[i] >= 0; k++) {
                dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
            }
        }
    }
    cout << dp[m];
    return 0;
}

二进制优化

将这sis_i件物品拆成系数构成一组价值和体积和系数乘积的物品,便可以将高问题转化为01背包问题。这些系数分别为1,2,4,......,2k1,n2k+11, 2, 4, ......, 2^{k-1}, n - 2^{k}+1,其中,k是满足n2k+1n-2^k+1的最大正整数。

例如,13可以拆解为1,2,4,61, 2, 4, 6

多重背包的二进制优化

#include <iostream>
#include <vector>
using namespace std;

const int N = 12010, M= 2010;
int v[N], w[N];
int dp[M];

int main() {
    int n, m;
    cin >> n >> m;
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        int vi, wi, si;
        cin >> vi >> wi >> si;
        // 系数拆解
        for (int k = 1; k <= si; k *= 2) {
            v[++cnt] = vi * k;
            w[cnt] = wi * k;
            si -= k;
        }
        if (si > 0) {
            v[++cnt] = vi * si;
            w[cnt] = wi * si;
        }
    }

    // 转化为01背包问题
    n = cnt;
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= v[i]; j--) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

单调队列优化

多重背包的单调队列优化

#include <iostream>
#include<cstring>

using namespace std;

const int N = 20010;

int n, m;
int f[N], g[N], q[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof(f));
        
        for (int j = 0; j < v; j++) {
            int hh = 0, tt = -1;
            for (int k = j; k <= m; k += v) { // k表示m%v的第几个数
                f[k] = g[k];
                if (hh <= tt && k-s*v > q[hh]) hh++;
                if (hh <= tt) f[k] = max(f[k], g[q[hh]]+(k-q[hh])/v*w);
                while(hh <= tt && g[q[tt]]-(q[tt]-j)/v*w <= g[k]-(k-j)/v*w) tt--;
                q[++tt] = k;
            }
        }
    }

    cout << f[m] << endl;
    return 0;
}

练习题

参考资料

  • 《剑指Offer专项突破版》