0/1背包变式

MaxNumberOfUsers

Problem Description
There is a server which has the disk space of M M M and the memory of N N N. Given some tasks, the i − t h i-th ithtask needs the disk space of X i X_i Xi and the memory of Y i Y_i Yi, and it can serve U i U_i Ui users. Design an algorithm and implement it to find out the maximum number of users that this server can serve simultaneously.

Input
The first line of input contains three integers M , N   a n d   K M, N\ and\ K M,N and K, which denote the total disk space, the total memory and the number of tasks. The next K K K lines are the disk space need, the memory need and the number of users served of tasks. ( 1 ≤ M , N ≤ 1000 , K ≤ 1000 , a n d   U i ≤ 100000 ) (1 ≤ M, N ≤ 1000 , K≤1000, and\ Ui ≤ 100000) (1M,N1000,K1000,and Ui100000)

Output
Line1: Maximum number of users

Sample Input

15 10 4

5 1 1000
2 3 3000
5 2 15000
10 4 16000

Sample Output

31000
一、题目解读

做个简单的解释,有两个大小的限制条件: M 、 N M、N MN,并给出了 K K K个任务,每个任务都会耗费一定的 M 、 N M、N MN,而每个任务都会带来不同的收益 V V V(题目中是人数 U i U_i Ui,这里我用 value 来代替,都是一个意思)。我们要求的就是在给定任务中计算出 磁盘空间和内存占用的总数找到不超过 M 、 N M、N MN时的最大收益值(每个只能取一次)。
对于上面测试样例,最大收益是 15000+16000,此时 M 和 N 分别还剩 0, 4 故不能继续添加任务来获得收益。
其实这是个“0/1 背包问题” 的演化。
(如果大家懂“0/1背包”问题,就直接跳到后面)

二、回归 0/1 背包问题

为了进行说明,先举一个简单的 “0/1 背包” 的例子。假设有小偷背着一个一共可以承载6kg的背包去偷金店(不要问我为什么他有没有同伙,也不要问为什么不拿个大包来偷,我更不是鼓励大家去偷),然后有三个物品的重量和价值分别是,翡翠:2kg,4 ; 金 砖 : 4 k g , 8 ;金砖:4kg,8 4kg8;钻石:3kg,6$。为了方便观察我们做成表格:

indexweight/kgvalue/$
124
248
336

怎么求最大收益?可以用枚举法,但是太麻烦。

  • csae1:看看最简单的问题会不会做,当index只有一个值的时候,我们只需要将 weight 与总重量 W 进行比较就可以,那么最简单的 case 是可以做。
  • case2:再难一点 index 较多的时候要怎么做?index 有 n 个的时候我不会做,那么 n-1 会不会做?发现还是不会,那么 n-1 与 n 物品在计算收益时有没有什么关系可以寻呢?而对于此问题,只有偷和不偷两种选择,因此我们可以有以下推断:

我们假设 index 有 n 个值,即有 n 件物品的时候小偷偷得的收益最大是 O P T ( n , W ) OPT(n,W) OPT(n,W)。 W 是当前背包容量。

  • 当第 k 个物品的重量大于当前容量时,小偷无法将第 k 个物品放入背包(不偷)此时最大收益 O P T ( k , W ) = O P T ( k − 1 , W ) OPT(k,W) = OPT(k-1,W) OPT(k,W)=OPT(k1,W)
  • 当第 k 个物品的重量小于等于当前容量时,小偷可以选择放,也可以选择不放入背包,当小偷不偷时, O P T ( k , W ) = O P T ( k − 1 , W ) OPT(k,W) = OPT(k-1,W) OPT(k,W)=OPT(k1,W),因为此时的收益和背包容量都没有变。如果小偷选择偷,那么 O P T ( k , W ) = O P T ( k − 1 , W − w k ) + v k OPT(k,W) = OPT(k-1,W-w_k)+v_k OPT(k,W)=OPT(k1,Wwk)+vk,也就是说当前的收益要加上第 k 个物品的 value,且容量要减去第 k 个物品的重量。

总上,我们有前 k 件物品的最大收益的状态转移方程:
O P T ( k , W ) = m a x { 0 , k < = 0 O P T ( k − 1 , W ) , n o t   s t e a l O P T ( k − 1 , W − w k ) + v k , s t e a l OPT(k,W)=max\begin{cases} 0, & k<=0 \\ OPT(k-1,W), & not\ steal\\ OPT(k-1,W-w_k)+v_k, & steal \end{cases} OPT(k,W)=max0,OPT(k1,W),OPT(k1,Wwk)+vk,k<=0not stealsteal

我们可以拿上面例子来进行说明(最多可以放 6kg)

indexweight/kgvalue/$
124
248
336
                            (3, 6)
                         /           \
                (2,6)                    (2,3)+6
                /   \                     /
          (1,6)      (1,3)+8            (1,3) 
          /  \          / \             /   \
     (0,6) (0,2)+4  (0,1) (0,2)+4   (0,3)  (0,1)+4

所以最大收益是 12,此时只拿 1 和 2 物品。
将状态转移方程对应到程序中,我们令 dp[i][j] 为前 i 个,且当前容量为 j 时的最大收益。

dp[i][j]  = max{ dp[i-1][j], dp[i-1][j-w[i]] + v[i] }

那么我们就可以针对“0/1背包”问题来写程序了:

for(int i=1; i<=n; ++i){    // 枚举物品
    for(int j=1; j<=W; ++j){        // 枚举背包容量
        if(j < w[i])
            dp[i][j] = dp[i - 1][j];
        else
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
    }
}
return dp[i][j]
indexweight/kgvalue/$
124
248
336

对照 for 循环我们有下表,i是指的第 i 个物品,j是指的当前背包容量,对应的值是当前的最大收益。

i/j0123456
00000000
10044444
200448812
3004481012

但是当数据过大的话,二维数组就比较浪费空间,所以我们可以降维,用一维数组来表达。

三、“0/1背包问题”优化

上面我们开了一个二维数组,但是我们注意到上面的表中,好多数据都是重复,没有必要去存储,我们只需要记录变化的点的值就可以,也就是说我们只保存对于 n 个物品,重量为 j 时收益最大的值。

dp[j] = max{ dp[j], dp[j-w[i]] + v[i]}

dp[j] 是指的重量不超过 j 可得到的最大收益。
**在用一维数组的时候一定要倒序遍历,不要正序。**max 中的 dp[i][j]是第 i-1 个的收益,如果正序遍历会更新值前面的值,这样就会导致后面的值的不准确,可能不是太好理解,我们继续拿前面的例子来进行说明:

indexweight/kgvalue/$
124
248
336

假设正序

for(int i=1; i<=n; ++i){    // 枚举物品
    for(int j=1; j<=W; ++j){        // 枚举背包容量
        if(j >= w[i])
            dp[j] = max{ dp[j], dp[j-w[i]] + v[i]};
    }
}

初始化为 0,最大重量为 6kg。
i == 1
dp[2] = max{dp[2], dp[2-w[1]]+v[1]} = max{0, 0+4} = 4
dp[3] = max{dp[3], dp[3-w[1]]+v[1]} = max{0, dp[1]+4} = 4
dp[4] = max{dp[4], dp[4-w[1]]+v[1]} = max{0, dp[2]+4} = max{0, 4+4} = 8

dp[6] = max{dp[6], dp[6-w[1]]+v[1]} = max{0, dp[4]+4} = max{0, 8+4} = 12

i == 2
dp[3] = max{dp[3], dp[3-w[2]]+v[2]} = max{4, 0} = 4
dp[6] = max{dp[6], dp[6-w[2]]+v[2]} = max{12, dp[2]+8} = max{12, 12} = 12

i == 3
dp[6] = max{dp[6], dp[6-w[3]]+v[3]} = max{12, dp[3]+6} = max{12, 10} = 12

我们发现当 i 为 1 时,结果已经不对了,正序 i 为 1 的时候,他居然可以装的价值是 12,而我们规定的是每个物品只能装一次,也就是当只有物品 1 并且背包为 6kg 时,value 最多只有 4。那 12 是怎么来的呢?在计算 dp[6] 的时候我们是比较的初始时的 dp[6] 和更新之后的 dp[4]+v[1],而跟新之后的 dp[4] 是已经偷了两次物品 1 之后的价值。所以dp[6] 就是指的偷了三次物品1之后的价值,这显然是不符合每件物品最多拿一次的要求的。
而最后 i=3 算出来 dp[6] 和二维数组的最大收益相同是因为,我们举出来的例子的最大收益是和完全背包一致的。也就是说,如果我们的背包放不满,假设背包是 8kg,那么最大收益就是 8+6 = 14(最大只能放4+3kg),而正序计算的话则为 8+8 =16(4+4kg).

indexweight/kgvalue/$
124
248
336

假设逆序

for(int i=1; i<=n; ++i){    // 枚举物品
    for(int j=W; j>=1; --j){        // 枚举背包容量
        if(j >= w[i])
            dp[j] = max{ dp[j], dp[j-w[i]] + v[i]};
    }
}
其实可以简化为
for(int i=1; i<=n; ++i){    // 枚举物品
    for(int j=W; j>=w[i]; --j){        // 枚举背包容量
            dp[j] = max{ dp[j], dp[j-w[i]] + v[i]};
    }
}

初始化为 0,最大重量 6kg。
i == 1
dp[6] = max{dp[6], dp[6-w[1]]+v[1]} = max{0, 4} = 4 (只放了一次)
dp[3] = max{dp[3], dp[3-w[1]]+v[1]} = max{0, 4} = 4
dp[2] = max{dp[2], dp[2-w[1]]+v[1]} = max{0, 0+4} = 4
i == 2
dp[6] = max{dp[6], dp[6-w[2]]+v[2]} = max{4, dp[2]+8} = 12
i == 3
dp[6] = max{dp[6], dp[6-w[3]]+v[3]} = max{12, 4+6} = 12

所以我们采用逆序遍历就可以节省空间复杂度。

四、回到原问题

前面道理懂了之后,就会发现原问题就是“0/1背包”的演变,“0/1背包”是只有一个 W 作为限制条件,而原问题是有两个限制条件。我们可以得到状态转移公式:

O P T ( k , M , N ) = m a x { 0 , k < = 0 O P T ( k − 1 , M , N ) , n o t   s t e a l O P T ( k − 1 , M − m k , N − n k ) + v k , s t e a l OPT(k,M,N)=max\begin{cases} 0, & k<=0 \\ OPT(k-1,M,N), & not\ steal\\ OPT(k-1,M-m_k,N-n_k)+v_k, & steal \end{cases} OPT(k,M,N)=max0,OPT(k1,M,N),OPT(k1,Mmk,Nnk)+vk,k<=0not stealsteal

for(int k=1; k<=K; ++k){
    for(int i=M; i>=m[k]; --i){
        for(int j=N; j>=n[k]; --j){
            dp[i][j] = dp[i][j] > dp[i-m[k]][j-n[k]]+v[k] ? dp[i][j] : dp[i-m[k]][j-n[k]]+v[k];
        }
    }
}
return dp[M][N];

在这里整个算法的时间复杂度是 O ( N M K ) O(NMK) O(NMK),空间复杂度是 O ( M N ) O(MN) O(MN).

#include<cstdio>
int dp[1010][1010] = {0};
int m[1010], n[1010], v[1010];

int Maxnumberofusers(int* param, int* m, int* n, int *v){
    if(m == nullptr || n == nullptr || v == nullptr)
        return 0;

    int M = param[0], N = param[1], K = param[2];
    for(int k=1; k<=K; ++k){
        for(int i=M; i>=m[k]; --i){
            for(int j=N; j>=n[k]; --j){
                dp[i][j] = dp[i][j] > dp[i-m[k]][j-n[k]]+v[k] ? dp[i][j] : dp[i-m[k]][j-n[k]]+v[k];
            }
        }
    }
    return dp[M][N];

}
int main(){
    int param[2];
    scanf("%d %d %d", &param[0], &param[1], &param[2]); // M N K

    for(int i=1; i<=param[2]; ++i)
        scanf("%d %d %d", &m[i], &n[i], &v[i]);
    int maxpeople = Maxnumberofusers(param, m, n, v);
    printf("%d", maxpeople);
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值