动态规划(多重背包):小S的货船租赁冒险 | 豆包MarsCodeAI刷题

72 阅读4分钟

题目链接

小S的货船租赁冒险

前置知识

01背包 & 完全背包

参考题解:
01背包题解
完全背包题解

三类背包问题的对比:

  • 01背包:物品只能选择一次
  • 完全背包: 物品选择无限制次数
  • 多重背包: 物品只能选择k次

多重背包是01背包的变形,抽象来看,01背包就是物品只能选择1次的多重背包
那实现多重背包其实就是在01背包的代码中再加入一层循环,遍历k个物品的选择情况
简单实现
背包容量n,物品数量m, 物品重量w数组, 价值v数组,数量c数组

vector<int>f(n+1);
for(int i=0;i < m; i++){
    for(int j = n; j >= w[i]; j--){
        for(int k = 1; k <= c[i] && k*w[i] <= j; k++ ){ //数量小于c[i],k个物品的重量不能超过当前的容量j
            f[j] = max(f[j], f[j-k*w[i]] + k * v[i]); //取原本的价值和选择k个物品i的最大值
        }
    }
}

OK, 三种背包就圆满落幕了,主要是理解思路,看不懂的请在评论区痛批(还是轻点)
回归本题

解题思路

  • 简化题意:
  • Q: 货船的种类数量。
  • V: 李华可用的总预算(单位:元)。
  • ships: 一个列表,其中每个元素是一个元组 [m[i], v[i], w[i]],分别表示第 i 种货船的数量、租赁价格和每艘货船的最大载货量。
    (题意挺简洁的,不需要简化了)
    几乎就是多重背包问题的模板了
    把V类比为容量,Q类比成物品数量,ships[i]数组的三元组分别对应物品的数量,重量和价值

那么代码其实就是上面的代码,换一下变量就可以了

代码

def solution(Q, V, ships):
    dp = [0] * (V + 1)

    for ship in ships:
        m, v, w = ship[0], ship[1], ship[2]

        for j in range(V, v - 1, -1):
            for k in range(1, m + 1):
                if j >= v * k:
                    dp[j] = max(dp[j], dp[j - v * k] + w * k)

    return dp[V]

优化[选读]

通过二进制分组优化多重背包

我们考虑朴素多重背包的时间复杂度, 大概是O(mwc)O(m*w*c)
其实O(mw)O(m*w)部分我们做不出啥优化,唯一能优化的就是最后一个cc
思想是把一个大数c转换成多位二进制来计算

比如c=5, 我们朴素做法会从1-5进行遍历
我们其实可以转换一下, 5 可以由 1 + 2 + 2得到
我们把物品数量拆出来, 拆成
list[1]=[w1,v1],list[2]=[w2,v2]list[3]=[w2,v2]list[1] = [w*1, v * 1], list[2] = [w*2, v * 2] list[3] = [w*2, v*2]
此时的list[i]代表直接选取i个物品,不需要在依次遍历了
模拟下这个过程 , 假设一个物品 v = 2 , w = 2, c = 5, 容量5 口算一下答案为5(4)
朴素算法需要 1 * 4 * 5 = 20次(大概,反正十多次)

  • 当前为list[1] = [2,2]

    • c = 5, 选择, f[5] = 2 + f[3] = 2
    • c = 4, 选择, f[4] = 2 + f[2] = 2
    • c = 3, 选择, f[3] = 2 + f[1] = 2
    • c = 2, 选择, f[2] = 2 + f[0] = 2
    • c = 1, 不能选择, f[1] 不变
  • 当前为list[2] = [4,4]

    • c = 5, 选择, f[5] = 4 + f[1] = 4
    • c = 4, 选择, f[4] = 4 + f[0] = 4
    • c = 3, 不能选择,f[3]不变
    • c = 2, 不能选择,f[2]不变
    • c = 1, 不能选择,f[1]不变
  • 当前为list[3] = [4,4] 与上一轮一样

明显可以看出二进制的情况下会比遍历少很多次重复计算, 数字越大越明显
特殊的 c= 10 , 拆成 1 + 2 + 4 + 3, 会出现一个非2k2^k的数字,我们在最后进行处理即可

// 拆分代码
    int v = 2, w = 2, c = 5;
    vector<pair<int,int>>list;
    int k = 1;
    while(c > k) {
        list.push_back({w *k, v * k});
        c -= k;
        k <<= 1; //k *= 2
    }
    if(c > 0) list.push_back({w *c, v * c});

拆分后,所有物品取n次都存在于了list数组中, 我们只需要跑01背包就行,此时list数组就是所有物品

具体代码,以本题为例

int solution(int Q, int V, vector<vector<int>> ships) {
    vector<pair<int, int>> list;
    for (int i = 0; i < Q; i++) {
        int c = ships[i][0];
        int w = ships[i][1];
        int v = ships[i][2];
        int k = 1;
        
        while (c > k) {
            list.push_back({w * k, v * k});
            c -= k;
            k <<= 1;
        }
        // 如果剩下的物品个数大于0,加入到列表
        if (c > 0)list.push_back({w * c, v * c});
    }
    vector<int> f(V + 1, 0);  
    for (int i = 0; i < list.size(); i++) {
        for (int j = V; j >= list[i].first; j--) {  // 从大到小更新
            f[j] = max(f[j], f[j - list[i].first] + list[i].second);
        }
    }
    return f[V];  
}