DP背包问题

最近学习了动态规划,网课上大佬们讲得我怎么都听不太懂???一听不懂就想摸鱼了...所以干脆不听课了,在这里整理一下经典DP题目。

一. 01背包问题

题目:P1048 [NOIP2005 普及组] 采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

01背包问题中,每个物品只有一件,换句话说,每件物品只有取(1)和不取(0)两种状态。

二维数组解决方法:

原理

——引自《算法竞赛——从入门到进阶》 

表格中f[i][j]第一维度i:待取背包的种类,第二维度j:待取背包的容量

我们每一次固定i,对j进行遍历。好比说我们对于一个物品i,路过每一个容量为j的背包时,我们既要问一下这个容量为j的背包:你要不要装一下物品i?

如果在背包容量为j时,j < w[i],那么此时肯定装不下物品i,那么在背包容量为j时,我们就不用考虑背包i了,考虑背包容量为j装不装物品i-1与此时情况相同,f[i][j]=f[i-1][j]

如果在背包容量为j时,j>=w[i],那么此时可以装的下物品i,我们就要考虑:装还是不装?

我们要取装和不装这两种情况的最大值,f[i][j]=max(f[i-1][j-w[i]]+v[i],f[i-1][j])

 上面的描述过程就是状态转移方程了。

优点:可以很好的记录选择的路径并打印出来

缺点:空间复杂度大。因为要开二维数组,当背包种类和容量较大时,需要开很大的数组。

#include<bits/stdc++.h>
using namespace std;
long long t[1005], v[105], f[105][1005], pre[105];//f代表在草药种类<=i且时间<=j时可以得到的最大价值
int main(void)                                   //pre数组记录第i种药草选择了没有
{
	int T, M;
	cin >> T >> M;//T采药时间,M药草种类
	for (int i = 1; i <= M; i++)
	{
		cin >> t[i] >> v[i];
	}
	for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
	{

		for (int j = 1; j <= T; j++)//枚举时间
		{
			if (j < t[i])//此时的时间装不下第i中药草,不选择
				f[i][j] = f[i - 1][j];
			else         //此时的时间装的下第i中药草
			{
				f[i][j] = max(f[i - 1][j - t[i]] + v[i], f[i - 1][j]);//既然装的下那么就有装和不装两种情况,取两种情况的最大值
			}
		}
	}
	cout << "采集药草的最大价值:" << f[M][T] << endl;
	cout << "采集药草的种类:";
	int i = M, j = T; int num = M;
	while (num--)//从后往前查找选择的背包
	{
		if (f[i][j] == f[i - 1][j - t[i]] + v[i])
		{
			pre[i] = 1;
			j = j - t[i];
			i = i - 1;
		}
		else
		{
			i = i - 1;
		}
	}
	for (int i = 1; i <= M; i++)
	{
		if (pre[i] == 1)
		{
			cout << i << ' ';
		}
	}
	return 0;
}

一维数组解决方法:

原理:滚动数组

滚动数组要直接从上面的二维数组的数学公式上理解有些困难。这里我直接举个例子用一维数组一层层展开解释。但不是数学上的严格证明。

假设背包容量是C=10

物品编号:1     2     3

物品重量:5     6     4

物品价值:20  10    12

f[] = 0 0 0 0 0 0 0 0 0 0

i=1:
f[10] = max(f[5]+20, f[10]);
f[9] = max(f[4]+20, f[9]);
f[8] = max(f[3]+20, f[8]);
f[7] = max(f[2]+20, f[7]);
f[6] = max(f[1]+20, f[6]);
f[5] = max(f[0]+20, f[5]);

f[] = 0 0 0 0 20 20 20 20 20 20
i=2:
f[10] = max(f[4]+10, f[10]);
f[9] = max(f[3]+10, f[9]);
f[8] = max(f[2]+10, f[8]);
f[7] = max(f[1]+10, f[7]);
f[6] = max(f[0]+10, f[6]);

f[] = 0 0 0 0 20 20 20 20 20 20
i=3:
f[10] = max(f[6]+12, f[10]);
f[9] = max(f[5]+12, f[9]);
f[8] = max(f[4]+12, f[8]);
f[7] = max(f[3]+12, f[7]);
f[6] = max(f[2]+12, f[6]);
f[5] = max(f[1]+12, f[5]);
f[4] = max(f[0]+12, f[4]);

f[] = 0 0 0 12 20 20 20 20 32 32

每一次遍历背包容量时需要从后往前遍历,这样每个背包信息只被用到一次(即只被拿一次),从前往后遍历时,背包信息会被用到多次(这就是后面要讲的完全背包问题)。

j>=w[i]时停止即可,因为当j<w[i]时,背包容量装不下第i个物品,所以选择不装,所以之后的f[i]就不用更新了,就相当于不装第i个物品了。

优点:空间复杂度小

缺点:不能很好的记录选择的路径,中间状态有缺失。

//滚动数组处理01背包问题
#include<bits/stdc++.h>
using namespace std;
long long t[1005], v[105], f[105];//f代表在时间<=i时可以得到的最大价值
int main(void)                                   
{
	int T, M;
	cin >> T >> M;//T采药时间,M药草种类
	for (int i = 1; i <= M; i++)
	{
		cin >> t[i] >> v[i];
	}
	for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
	{

		for (int j = T; j >= t[i]; j--)//从后往前枚举时间
		{
			f[j] = max(f[j], f[j - t[i]] + v[i]);
		}
	}
	cout <<"采集药草的最大价值:"<< f[T] << endl;
	return 0;
}


二.完全背包问题

题目:   P1616 疯狂的采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

与01背包问题不同之处,完全背包问题中待取物品每个都有无限件。所以状态转移方程也不一样。

上面解释滚动数组原理时提到过,如果j从前往后遍历每个背包将会被计算多次,利用这一点可以解决完全背包问题。

假设背包容量是C=10

物品编号:1     2     3

物品重量:5     6     4

物品价值:20  10    12

f[] = 0 0 0 0 0 0 0 0 0 0 

i=1:
f[5] = max(f[0]+20, f[5]);
f[6] = max(f[1]+20, f[6]);
f[7] = max(f[2]+20, f[7]);
f[8] = max(f[3]+20, f[8]);
f[9] = max(f[4]+20, f[9]);
f[10] = max(f[5]+20, f[10]);

f: 0 0 0 0 20 20 20 20 20 40 //在已经拿过背包1后,f[10]又由f[5]决定了,f[5] = f[0] + 20,
f[5]也已经拿过背包1了!f[10]这样计算拿过2次背包1!!!
i=2:

f[6] = max(f[0]+10, f[6]);
f[7] = max(f[1]+10, f[7]);
f[8] = max(f[2]+10, f[8]);
f[9] = max(f[3]+10, f[9]);
f[10] = max(f[4]+10, f[10]);

f[] = 0 0 0 0 20 20 20 20 20 40
i=3:
f[4] = max(f[0]+12, f[4]);
f[5] = max(f[1]+12, f[5]);
f[6] = max(f[2]+12, f[6]);
f[7] = max(f[3]+12, f[7]);
f[8] = max(f[4]+12, f[8]);
f[9] = max(f[5]+12, f[9]);
f[10] = max(f[6]+12, f[10]);

f: 0 0 0 12 20 20 20 24 32 40
//完全背包问题
#include<bits/stdc++.h>
using namespace std;
long long t[10000005], v[10005], f[10005];//f代表在时间<=i时可以得到的最大价值
int main(void)
{
	int T, M;
	cin >> T >> M;//T采药时间,M药草种类
	for (int i = 1; i <= M; i++)
	{
		cin >> t[i] >> v[i];
	}
	for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
	{

		for (int j = t[i]; j <= T; j++)//从前往后枚举时间
		{
			f[j] = max(f[j - t[i]] + v[i], f[j]);
		}
	}
	cout << f[T] << endl;;

	return 0;
}

由此可见,01背包与完全背包问题都可以用一维数组解决,不同的是:

01背包问题要从后往前枚举背包容量

完全背包问题要从前往后枚举背包容量

完全背包问题用二维数组解决需要用到三重循环

	for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
	{

		for (int j = t[i]; j <= T; j++)//枚举时间
		{
			for (int z = 1; z * t[i] <= j; z++)//枚举药草i采取的数量
			{
				f[i][j] = max(f[i - 1][j - z * t[i]] + z * v[i], f[i - 1][j]);
			}
		}
	}

采用01背包打印方案的方法也可以打印方案


三.多重背包问题

题目:HDU 2191 

与完全背包不同之处在于,每个物品的数量不是无限,而是有限个。

我们可以将多重背包问题转化为01背包问题。例如:一个物品的重量是2,价值是3,一共有5个。我们可以把它看成5个重量是2,价值是3的“不一样”的物品。

#include<bits/stdc++.h>
using namespace std;
int P[2005], H[2005], f[105];//p价格,h重量
int main(void)
{
	int C;
	cin >> C;
	while (C--)
	{
		int n,m;
		cin >> n >> m;//n经费金额m大米种类
		int cnt = 1;
		for (int i = 1; i <= m; i++)
		{
			int p, h, c;
			cin >> p >> h >> c;
			while (c--)//转化成01背包问题
			{
				P[cnt] = p;
				H[cnt] = h;
				cnt++;
			}
		}
		for (int i = 1; i < cnt; i++)
		{
			for (int j = n; j >= P[i]; j--)
			{
				f[j] = max(f[j], f[j - P[i]] + H[i]);
			}
		}
		cout << f[n] << endl;
	}
	return 0;
}

四.分组背包问题

题目:9. 分组背包问题 - AcWing题库

思路:将每个组视为01背包中的物品种类,最内层对每个组内的物品进行遍历。

//分组背包
#include<bits/stdc++.h>
using namespace std;
int num[105],w[105][105], v[105][105],f[105];//num[i]:第i组有多少物品, w[i][j]第i组第j个物品的价值,f[i]背包容量<=i时能装的最大价值
int main(void)
{
	int N, V;
	cin >> N >> V;//N物品组数V背包容量
	for (int i = 1; i <= N; i++)
	{
		cin >> num[i];
		for (int j = 1; j <= num[i]; j++)
		{
			cin >> v[i][j] >> w[i][j];
		}
	}
	for (int i = 1; i <= N; i++)//枚举组数
	{
		for (int j = V; j >= 1; j--)//从大到小枚举容量
		{
			for (int z = 1; z <= num[i]; z++)//枚举每一组内的物品
			{
				if(j >= v[i][z])
				f[j] = max(f[j], f[j - v[i][z]] + w[i][z]);
			}
		}
	}
	cout << f[V];
	return 0;
}

补充一道分组背包的好题:

P5322 [BJOI2019]排兵布阵 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 思路:将第i个城堡看做物品i,城堡i中有s个玩家布置的兵力,即物品i中有s个不同的物品样式。

背包容量是m求最大值。洛谷上的题解说的也挺明白的。

#include<bits/stdc++.h>
using namespace std;
int solder[105][105],a[105][105],f[20005];//a数组表示第i个城堡中玩家1-j士兵的情况
int main(void)
{
	int s, n, m;//分别表示玩家人数、城堡数和每名玩家拥有的士兵数。
	cin >> s >> n >> m;
	for (int i = 1; i <= s; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			cin >> solder[i][j];
		}
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= s; j++)
		{
			a[i][j] = solder[j][i];
		}
	}
	for (int i = 1; i <= n; i++)
	{
		sort(a[i] + 1, a[i] + 1 + s);//每个城堡的兵力人数排序
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = m; j >= 0; j--)
		{
			for (int z = 1; z <= s; z++)
			{
				if (j > a[i][z] * 2)
				{
					f[j] = max(f[j], f[j - 2 * a[i][z] - 1] + i * z);
				}
			}
		}
	}
	cout << f[m];
	return 0;
}

五.超大背包问题

目录

一. 01背包问题

二维数组解决方法:

一维数组解决方法:

二.完全背包问题

三.多重背包问题

四.分组背包问题

五.超大背包问题


 优秀博文:(89条消息) hdu 5887 Herbs Gathering_HopeForBetter的博客-CSDN博客

#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
int N, ind;
long long  V, ans;
struct Node {
	long long weight, value;
	double per;
};
Node thing[100];
bool cmp(const Node p1, const Node p2) { return p1.per > p2.per; }//按背包单位价值从高到低排序
bool check(int i, long long now_v, long long now_w)//计算从背包i算起,理论的最大价值(剩下的物品已经按单位价值提前排序,所以只要背包容量允许“见一个拿一个”,补上最后的背包剩余重量,所得价值一定是理论背包最大价值)
{
	for (int j = i; j < ind; j++)
	{
		if (thing[j].weight + now_w <= V)
		{
			now_v += thing[j].value;
			now_w += thing[j].weight;
		}
		else
		{
			now_v += int((V - now_w) * thing[j].per);
		}
	}
	return now_v > ans;
}
void dfs(int i, long long now_v,long long now_w)//i:当前考虑到第i个物品,now_v:前i个物品价值,now_w:前i个物品重量
{
	ans = max(ans, now_v);//ans记录最大价值
	if (i >= ind || !check(i, now_v, now_w))//搜索的背包已经到头或者从这个背包开始理论最大价值已经小于ans,则退出
		return;
	if (now_w + thing[i].weight <= V)
		dfs(i + 1, now_v + thing[i].value, now_w + thing[i].weight);//选第i个物品
	dfs(i + 1, now_v, now_w);//不选第i个物品
	return;
}
int main(void)
{
	scanf("%d", &N);
	scanf("%lld", &V);
	for (int i = 1; i <= N; i++)
	{
		long long a, b;
		scanf("%lld%lld", &a, &b);//体积、价值
		if (a <= V)
		{
			thing[ind].weight = a;
			thing[ind].value = b;
			thing[ind].per = b * 1.0 / a;
			ind++;
		}
	}
	sort(thing, thing + ind, cmp);
	dfs(0, 0, 0);
	printf("%lld", ans);
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值