动态规划优化问题-4

例三:多重背包

​ 多重背包问题我们经常能碰到,就是给出n种物品,每种物品有a[i]个,占用空间为v[i],价值为w[i],背包容量为K,问背包所能装下的物品的最大价值

方法一:

​ 我们一般的求法就是开一个dp[k]数组代表空间容量为i时的最大价值。然后遍历每种物品的每件,通过递推公式:

dp[k] = max(dp[k], dp[k - v[i]] + w[i]);

​ 这时时间复杂度为O(n * m * K)(其中n为种类数,m为每种物品的数量,K为背包容量)

​ 但是,当m特别大时,我们应该怎么办呢?

方法二:

​ 我们会发现,每种的物品,它们的v[i]和w[i]是相同的。假如某种物品有15种,我们上面那种方法的做法,由于是遍历该种的每一件物品,因此也就是将{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}任意组合。我们会发现组合会有很多重复,因此我们可以这样进行优化:

​ 对于集合{1, 2, 4, 8},我们也可以通过这四个数组合成1~15个的任意一个。这样我们只需要求每种物品的n件物品优化成logn种物品,这样时间复杂度就可以优化为O(n * log(m) * K)

​ 注意点!!!!如果一种物品有n件,所选的集合必须只能组成[1, n],比如13:{1, 2, 4, 6}

​ 这种方法的代码如下:

#include <stdio.h>
#include <algorithm>
using namespace std;

int dp[10005] = {0};

int main () {
    int n, K, v, w, num;
    scanf("%d%d", &K, &n);		//K代表背包容量,n代表物品种类数
    for (int i = 0; i < n; i++) {
        scanf("%d%d%d", &v, &w, &num);	//v,w,num分别代表每种物品的体积,价值,数量
        int val = 1;		//val为集合所有元素的和,初始化为1
        int j = 1;			//j为集合为集合中的元素,初始化为1
        while (val <= num) {
            int vv = v * j;	  //此时把j个该种物品看作一个物品进行dp,也就是01背包
            int ww = w * j;
            for (int p = K; p >= vv; p--) {	
                dp[p] = max(dp[p], dp[p - vv] + ww);
            }
            j = val + 1;
            val += j;
        }
        val -= j;			//该操作保证所求集合确保正好组成[1, num]
        j = num - val;
        if (j > 0) {
            int vv = v * j;
            int ww = w * j;
            for (int p = K; p >= vv; p--) {
                dp[p] = max(dp[p], dp[p - vv] + ww);
            }
        }
    }
    printf("%d\n", dp[K]);
    return 0;
}

​ 方法二时间复杂度为O(n * logm * K),还不可以继续优化呢?答案是可以的,我们可以利用单调队列对多重背包问题进行优化到O(n * K)。大家可以先自己考虑一下如何去做再往下看

方法三:

​ 方法三看了好多好的题解才搞懂。。只能说自己太笨了Orz~以下是我的理解,希望能对大家有些帮助

​ 我们单拿出一种物品来看,比如体积为5,价值为6,数量为3,再假如K为22。那么我们单拿出全部%5 == 2的容量来看:

dp[2] = dp[2];
dp[7] = max(dp[2], dp[7] - 6) + 6;
dp[12] = max(dp[2], dp[7] - 6, dp[12] - 12) + 12;
dp[17] = max(dp[2], dp[7] - 6, dp[12] - 12, dp[17] - 18) + 18;
dp[22] = max(dp[7] - 6, dp[12] - 12, dp[17] - 18, dp[22] - 24) + 24;

//这一步没看懂的同学看这里!!
比如dp[22] = max(dp[7] - 6, dp[12] - 12, dp[17] - 18, dp[22] - 24) + 24中,
dp[7] - 6若是最大值,dp[7] - 6 + 24等价于dp[22] = dp[7] + 18,也就是用3个该种物品
dp[12] - 12若是最大值,dp[12] - 12 + 24等价于dp[22] = dp[12] + 12,也就是用2个该种商品
dp[17] - 18若是最大值,dp[17] - 18 + 24等价于dp[22] = dp[17] + 6,也就是用1个该种商品
dp[22] - 24若是最大值,dp[22] - 24 + 24等价于dp[22] = dp[22],也就是不用该种商品

​ 至于最多选用几个的数量s,它等于s = min(s, K / v)。这样,我们就可以将题目转化为下面的代码形式(伪代码):

for (循环遍历每种商品) {
    for (循环遍历全部余数d[0, v)) {
        for (循环遍历K以内全部余数为d的容量k*v+d) {
          	for (循环遍历s个上一种物品的状态并更新最大值) {
                
          	}
        }
    }
}

我们从伪代码可以看出时间复杂度为O(n * K * s) (第二层和第三层for相乘为K),和上一种方法复杂度差不多甚至在某些情况下还要更高。我们可以用单调队列维护最大值且区间差值小于等于s来优化该算法,此时复杂度为O(n * K)

​ 比如说:

将dp[2] - 0 * 6加入单调队列并维护最大值,然后用队列最大值 + 0 * 6即为新的dp[2]
将dp[7] - 1 * 6加入单调队列并维护最大值,然后用队列最大值 + 1 * 6即为新的dp[7]
将dp[12] - 2 * 6加入单调队列并维护最大值,然后用队列最大值 + 2 * 6即为新的dp[12]
将dp[17] - 3 * 6加入单调队列并维护最大值,然后用队列最大值 + 3 * 6即为新的dp[17]
将dp[22] - 4 * 6加入单调队列并维护最大值,然后用队列最大值 + 4 * 6即为新的dp[22]

//注意单调队列的维护需要注意的两点(1.若q[tail] <= val则循环tail--直到满足条件为止 2.若q[tail]与q[head]相差数量大于s则循环head++直到满足条件为止)

​ 下面给出这道题单调队列优化的代码:

#include <stdio.h>
#include <algorithm>
using namespace std;
//dp[i]存储当前状态下容量为i的最大价值
//q[i]用于模拟优先队列
//inv[i]用于存储优先队列中位置为i的使用该种商品的物品数
int dp[10005] = {0}, inv[10005] = {0}, q[10005] = {0};

int main () {
    int K, n, v, w, s;
    scanf("%d%d", &K, &n);
    for (int i = 0; i < n; i++) {
        scanf("%d%d%d", &v, &w, &s);
        s = min(K / v, s);    //代表能用的该种物品的最大数量
        for (int mod = 0; mod < v; mod++) {
            int head = 1, tail = 1; //相当于清空队列
            for (int j = 0; j <= (K - mod) / v; j++) {
                int val = dp[j * v + mod] - j * w;
                while (head < tail && q[tail - 1] <= val) {
                    tail--;         //该操作保证队列单调
                }
                q[tail] = val;
                inv[tail++] = j;
                while (head < tail && j - inv[head] > s) {
                    head++;         //该操作用于维护数量上限
                }
                //此时队列的头(即最大值)就是我们需要使用该种商品的数量
                dp[j * v + mod] = max(q[head] + j * w, dp[j * v + mod]);
            }
        }
    }
    printf("%d\n", dp[K]);
    return 0;
}

转载请注明出处!!!

如果有写的不对或者不全面的地方 可通过主页的联系方式进行指正,谢谢

猜你喜欢

转载自blog.csdn.net/Ivan_zcy/article/details/86511863
今日推荐