进击高手【第七期】背包dp

引入

众所周知,背包也为线性dp中不可缺少的一环

定义

背包问题(Knapsack problem)是一种组合优化的问题。
问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。

分类

  • 01背包问题
  • 完全背包问题
  • 多重背包问题
  • 混合三种背包问题
  • 分组背包问题
  • 二维费用背包问题

01背包问题

N N N 件物品和一个容量为 V V V 的背包。放入第i件物品耗费的体积是 v i v_{i} vi ,得到的价值是 p i p_{i} pi.
求解:将哪些物品装入背包可使价值总和最大?

解决策略:

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即 F i , v F_{i, v} Fi,v 表示前i件物品放入一个容量为v 的背包可以获得的最大价值。
F[i,v]=max{F[i −1, v], F[i −1,v−w_i] +P_i } ( v >=w_i)

伪代码如下
在这里插入图片描述
我们可以发现我们能降低空间复杂度
优化一: 滚动数组

k=1;
for(int i=1; i<=n; i++)
{
    for(int j = 0; j <= V; j++)
    {    f[k][j] = f[1-k][j];	//不放第i件物品
         if(j >= w[i]) 
            if(f[1-k][j-w[i]] + v[i] > f[k][j])
                f[k][j] = f[1-k][j-w[i]] + v[i];	 //放第i件物品
     }
     k = 1-k;    //滚动一次,为下一阶段计算作准备
}

在这里插入图片描述

伪代码如下

在这里插入图片描述

完全背包问题

N N N 种物品和一个容量为 V V V 的背包,每种物品都有无限件可用。放入第 i i i 种物品的费用是 C i C_{i} Ci ,价值是 W i W_{i} Wi 。求将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。

我们很容易拿到这个伪代码
在这里插入图片描述
然后类似的一维数组优化为
在这里插入图片描述

多重背包问题

N N N 种物品和一个容量为 V V V 的背包。第 i i i 种物品最多有 M i M_{i} Mi 件可用,每件耗费的空间是 C i C_{i} Ci ,价值是 W i W_{i} Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

这个问题非常类似于 01 背包问题,不同的是每种物品有有限件。我们完全可以把它转化为 01 背包来解答,也就是把同类有限件物品,每件看成独立的物品用01背包处理,这样处理的话时间复杂度 O ( N ∗ Σ n u m i ∗ V ) O(N * \Sigma numi * V) O(NΣnumiV) 1 ≤ i ≤ n 1 \leq i \leq n 1in.

在这里插入图片描述

但这时间复杂度太高了, 我们可以使用 二进制 优化

在这里插入图片描述

混合背包问题

如果将前面 1 、 2 、 3 中的三种背包问题混合起来。也就是说,有的物品只可以取一次( 01 背包) ,有的物品可以取无限次(完全背包) ,有的物品可以取的次数有一个上限(多重背包) 。应该怎么求解呢?

01背包状态转移方程:
在这里插入图片描述

完全背包状态转移方程:
在这里插入图片描述

我们通过观察上面两种背包的状态转移方程不难发现它们枚举顺序不一样以外,其余是一样的,而多重背包也可以通过 二进制优化 转化为 01背包 ,那么我们完全可以用一个数组来区分该类物品到底属于 01背包 解决还是 完全背包 解决。两个步骤完成混合背包:
1、先处理物品,把它们归为01背包和完全背包两类;
2、使用01背包和完全背包的状态转移方程完成最大价值的求解。

  1. 处理物品
    在这里插入图片描述
  2. 状态转移
    在这里插入图片描述

分组背包问题

N N N 件物品和一个容量为 V V V 的背包。第 i i i 件物品的费用是 C i C_{i} Ci ,价值是 W i W_{i} Wi 。这些物品被划分为 K K K 组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

由于每组物品最多只能拿一件,我们可以把“物品组数”作为 D P DP DP 的阶段,单纯的 01背包 一个阶段只有唯一的一个状态,而分组背包每组有 n u m i num_{i} numi 件物品,我们可以循环 n u m i num_{i} numi 次来最终确定该阶段的最优解,因此,在单纯的 01背包 两重循环的基础上还需要添加一重循环来枚举每组物品里的每一件物品,用来更新最优解。

代码如下

在这里插入图片描述

二维费用背包问题

对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量) 。问怎样选择物品可以得到最大的价值。设第 i i i 件物品所需的两种费用分别为 C i C_{i} Ci D i D_{i} Di 。两种费用可付出的最大值(也即两种背包容量)分别为 V V V U U U . 物品的价值为 W i W_{i} Wi

费用加了一维,只需状态也加一维即可。设 F[i,v,u] 表示前 i 件物品付出两种费用
分别为 v 和 u 时可获得的最大价值。状态转移方程就是:
F[i, v, u] = max{F[i − 1, v, u], F[i − 1, v − C i C_{i} Ci , u − D i D_{i} Di ] + W i W_{i} Wi }

例题

  1. 开心的金明

一个很标准的 01 背包

  1. Investment

一道完全背包。注意到债券的价值始终是 1000 的倍数, 我们可以初始化时先 / 1000 /1000 /1000 套模板就不会爆数组。

核心代码

#include <bits/stdc++.h>
using namespace std;
int  t, s, n ,d;
int w[5005], c[5005], dp[100005]; 
int main(){
	scanf("%d", &t);
	while(t--){
		scanf("%d %d %d", &s, &n, &d);
		for(int i = 1; i <= d; i++)
			scanf("%d %d", &w[i], &c[i]);
		for(int k = 1; k <= n; k++){
			for(int i = 1; i <= d; i++)
				for(int j = w[i] / 1000; j <= s / 1000; j++)
					dp[j] = max(dp[j], dp[j - w[i] / 1000] + c[i]);
			s += dp[s / 1000];
			memset(dp, 0, sizeof(dp));
		} 
		printf("%d\n", s);
	}
    return 0;
}
  1. 宠物小精灵之收服
    我在这篇文章讲了不多赘述。

  2. Space Elevator 太空电梯

显然,这是一道多重背包的动态规划题目。

部分代码

bool cmp(node x, node y){
	return x.a < y.a;
}
for(int i = 1; i <= n; i++)
		scanf("%d %d %d", &p[i].h, &p[i].a, &p[i].c);
	sort(p + 1, p + 1 + n, cmp);
	for(int i = 1; i <= n; i++){
		if(p[i].h > p[i].a)
			continue;
		for(int j = 1; j <= p[i].c; j++)
			for(int k = p[i].a; k >= p[i].h; k--)
				if(dp[k - p[i].h] + p[i].h <= k){
					dp[k] = max(dp[k], dp[k - p[i].h] + p[i].h);
					ans = max(dp[k], ans);
				}
	}
  1. 潜水员

本题与二维费用的背包问题有所不同,一般的二维费用背包问题,是指花费1和花费2不超过某个数的最大价值。而本题相反,需要求出满足氧气和氮气体积需要的前提下所带气缸的最小重量,抽象一下就是花费1和花费2不小于某数的最小价值。
所以便有了一下代码:

#include<bits/stdc++.h> 
using namespace std;
const int Max = 2005;
int u, v, n, a[Max], b[Max], c[Max];
int dp[Max][Max];
int main(){
	memset(dp, 0x3f, sizeof(dp));
    scanf("%d %d %d", &u, &v, &n);
    for(int i = 1; i <= n; i++)
    	scanf("%d %d %d", &a[i], &b[i], &c[i]);
    dp[0][0] = 0;
    for(int i = 1; i <= n; i++)
    	for(int j = u + 100; j >= a[i]; j--)
    		for(int k = v + 100; k >= b[i]; k--)
    			dp[j][k] = min(dp[j][k], dp[j - a[i]][k - b[i]] + c[i]);
	for(int i = u; i <= u + 100; i++){
		for(int j = v; j <= v + 100; j++){
			if(dp[i][j] != 0x3f3f3f3f){
				printf("%d", dp[i][j]);
				return 0;
			}
		}
	}
	puts("-1");
    return 0;
}

end

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值