动态规划解决背包问题:01背包及延申(超易懂解释)

本文深入探讨了背包问题的不同类型,包括01背包、完全背包、多重背包等,并通过实例详细解析了动态规划的解题思路,同时介绍了空间优化技巧。此外,还涉及了最长公共子序列、找零钱、打家劫舍等问题的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先从最简单的01背包入手来理解背包问题,再进行延申解决完全背包、多重背包的问题,最长公共子序列的问题。我的代码有挺多多余的部分,主要是为了配合解析的思路写的。

问题1:01背包

我们有一个容量为x 的背包,有重量为数组w和对应价值为数组v的物件,解决如何使背包里的物件总价值最高的问题。
为了简单理解,假设只有三个物件:w={2,4,3};v={3,7,5};
我们的背包容量假设为8;
我们尝试用三个管理员来对应地管理这三个物件:

管理员0说:我就只看这个背包装第1个物件是啥情况,啊因为一个物件只能放一次,所以容量大于2之后就是这个样子的嘛。。

背包重量背包价值012345678
23003333333

管理员1说:那我也就只看这个背包装第1个物件是啥情况。欸?不对,管理员0已经装好了他对应的背包,那我每次都最好偷偷懒看看他那边的情况,显然当背包容量小于我这个物件的重量的时候我就直接跟着管理员0怎么装的来装就好了。那如果能装我这个物件了,那我就瞅瞅当背包装了我这个物件之后的价值:v[1]+dp[0][j-w[1]],其中 dp[0][j-w[1]] 代表了装了一个我这个物件时装物件0 的价值(擦看了管理员0的价值表),那么我这个物件装上价值高呢还是不装价值高呢?那就看看两种情况哪种价值更高吧:max(dp[0][j],v[1]+dp[0][j-w[1]])(其中j对应的是当前背包容量。)

背包重量背包价值012345678
47003377101010

管理员2说:那我懂了,我也跟着管理员1偷懒,我就看管理员1的就好啦哈哈哈
v[2]+dp[1][j-w[2]]max(dp[1][j],v[2]+dp[1][j-w[2]])

因此就有C++代码:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<vector<int>> solve(vector<int> v, vector<int> w, int x) {
	vector<vector<int>> dp;//所有管理员结合起来的数组
	vector<int> t0;//管理员0的数组
	for (int j = 0; j <= x; j++) {
		if (j < w[0]) {
			t0.push_back(0);
		}
		else {
			t0.push_back(v[0]);
		}
	}
	dp.push_back(t0);//把管理员0的数组加入到最终的二维数组中
	for (int i = 1; i < v.size(); i++) {
		vector<int> ti;
		for (int j = 0; j <= x; j++) {
			if (j < w[i]) {//装不了当前的物件就参考上一个管理员怎么装的
				ti.push_back(dp[i - 1][j]);
			}
			else {//可以装当前的物件了,参考上一个管理员,怎么装最大就怎么装
				ti.push_back(max(dp[i - 1][j], v[i] + dp[i - 1][j - w[i]]));
			}
		}
		dp.push_back(ti);
	}
	for (auto i : dp) {//打印一下dp表
		for (auto j : i) {
			cout << j << "\t";
		}
		cout << endl;
	}
	return dp;
}
int main() {
	vector<int> w = { 2,4,3 };
	vector<int> v = { 3,7,5 };
	vector<vector<int>> dp = solve(v, w, 8);
	system("Pause");
	return 0;
}

运行结果
在这里插入图片描述

问题2:空间优化问题:

领导来了,看到这群偷懒的管理员指着鼻子骂道:你们这群懒猪,一个人能干的活儿你们这么搞,浪费公司资源啊。管理员0逮到机会了说:领导,这个项目我自己搞就行了,活儿我一人能干!然后其他管理员就被迫下岗了。。。

管理员0是这么解决的:与其让他们参考我的工作成果,不如我自己把我上一次装的方案记录下来,然后自己再看。我简直就是一个天才。

int solve(vector<int> v, vector<int> w, int x) {
	vector<int> t0;//管理员0的数组
	int res = 0;//存储最终结果
	for (int j = 0; j <= x; j++) {
		if (j < w[0]) {t0.push_back(0);}
		else {t0.push_back(v[0]);}
	}
	for (int i = 1; i < v.size(); i++) {
		vector<int> ti=t0;//存储上一次记录的结果
		t0.clear();
		for (int j = 0; j <= x; j++) {
			int t;
			if (j < w[i]) {
				t = ti[j];
				t0.push_back(t);
				res = max(res, t);
			}
			else {
				t = max(ti[j], v[i] + ti[j - w[i]]);
				t0.push_back(t);
				res = max(res, t);
			}
		}
	}
	return res;
}

问题3:完全背包

完全背包问题就是指每一个物件可以放的次数是随便的,使用同样的数据:
w={2,4,3};v={3,7,5};
我们的背包容量假设为8;
此时多了一个循环,即当背包容量大于当前物件重量时,要查看可以装多少个当前的物件,对应地上一个物件就要减少相应分配给上一个物件的容量空间。
核心就在这一段代码里:

		for (int j = 0; j <= x; j++) {//x为背包的容量
			int t;
			if (j < w[i]) {//此时装不下当前背包
				t = ti[j];//按上一个物件的记录来记录当前物件
				t0.push_back(t);
			}
			else {//循环查看装k个当前物件时的最大价值并将最大价值放入列表
				for (int k = 1; k*w[i] <= j; k++) {
					t = max(ti[j], k*v[i] + ti[j - k*w[i]]);
				}
				t0.push_back(t);
			}
			res = max(res, t);//记录当前的最大值
		}

这是填表的情况:
在这里插入图片描述

空间优化的代码:

int solve(vector<int> v, vector<int> w, int x) {
	vector<int> t0;//管理员0的数组
	int res = 0;
	for (int j = 0; j <= x; j++) {
		if (j < w[0]) {t0.push_back(0);}
		else {
			int t = v[0];
			for (int k = 2; k*w[0] <= j; k++) {
				t += v[0];
			}
			t0.push_back(t);
		}
	}
	for (int i = 1; i < v.size(); i++) {
		vector<int> ti=t0;
		t0.clear();
		for (int j = 0; j <= x; j++) {
			int t;
			if (j < w[i]) {
				t = ti[j];
				t0.push_back(t);
				res = max(res, t);
			}
			else {
				for (int k = 1; k*w[i] <= j; k++) {
					t = max(ti[j], k*v[i] + ti[j - k*w[i]]);
				}
				t0.push_back(t);
				res = max(res, t);
			}
		}
	}
	return res;
}

问题4:多重背包

多重背包问题就是加了一项,也就是每个物件有指定的个数:
w={2,4,3};v={3,7,5};n={3,1,2};
同样地取背包容量x=8
在理解了完全背包之后,就更容易解决多重背包的问题了
为了便于观察,这里贴出的是没有进行空间优化的代码:

vector<vector<int>> multibags(vector<int> v, vector<int> w,vector<int> n, int x) {
	vector<vector<int>> dp;//所有管理员结合起来的数组
	vector<int> t0;//管理员0的数组
	for (int j = 0; j <= x; j++) {
		if (j < w[0]) { t0.push_back(0); }
		else {
			int t = v[0];
			for (int k = 2; k*w[0] <= j&&k<=n[0]; k++) {
				t += v[0];
			}
			t0.push_back(t);
		}
	}
	dp.push_back(t0);//把管理员0的数组加入到最终的二维数组中
	for (int i = 1; i < v.size(); i++) {
		vector<int> ti;
		for (int j = 0; j <= x; j++) {
			if (j < w[i]) {//装不了当前的物件就参考上一个管理员怎么装的
				ti.push_back(dp[i - 1][j]);
			}
			else {//可以装当前的物件了,参考上一个管理员,怎么装最大就怎么装
				int t = dp[i - 1][j];
				for (int k = 1; k*w[i] <= j&&k<=n[i]; k++) {
					t = max(t, k*v[i] + dp[i - 1][j - k*w[i]]);
				}
				ti.push_back(t);
			}
		}
		dp.push_back(ti);
	}
	for (auto i : dp) {//打印一下dp表
		for (auto j : i) {
			cout << j << "\t";
		}
		cout << endl;
	}
	return dp;
}

运行填表结果图
在这里插入图片描述

问题5:最长公共子序列

给定两个字符串,求解这两个字符串的最长公共子序列(Longest Common Sequence)。比如字符串1:BADXCATBA;字符串2:ABCBTDXAB

则这两个字符串的最长公共子序列长度为4,最长公共子序列是:ADXAB
类似于背包问题,将大问题分为几个子问题进行填表
有这样一个递归公式:
在这里插入图片描述

void LCS(string a, string b) {
	int al = a.length(), bl = b.length();
	vector<vector<string>> tempLCS;
	vector<vector<int>> dp;
	vector<int> t0( al + 1,0);
	vector<string> ts(al+1,""); 
	for (int i = 0; i <= bl; i++) { dp.push_back(t0); tempLCS.push_back(ts); }
	for (int i = 1; i <= bl; i++) {
		for (int j = 1; j <= al; j++) {
			if (a[j - 1] == b[i - 1]) {
				dp[i][j] = dp[i - 1][j - 1] + 1;
				tempLCS[i][j] = tempLCS[i - 1][j - 1] + a[j - 1];
			}
			else {
				dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
				if (tempLCS[i - 1][j].length() >= tempLCS[i][j - 1].length()) {
					tempLCS[i][j] = tempLCS[i - 1][j];
				}
				else {
					tempLCS[i][j]= tempLCS[i][j - 1];
				}
			}
		}
	}
	show(tempLCS);
	show(dp);
}

运行结果:
在这里插入图片描述

问题6:找零钱

有数组cash,cash中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数tar(小于等于1000)代表要找的钱数,求换钱有多少种方法。
即:给定数组cash及它的大小(小于等于50),同时给定一个整数tar,请返回有多少种方法可以凑成tar。
测试用例cash={2,5,10},tar=15;
其中的递归解为:
dp[i]=[dp[i-cash[j]] for j in cash]
如图为dp表:
在这里插入图片描述
代码直接打印出了dp表:

void findcash(vector<int> cash, int tar) {
	sort(cash.begin(), cash.end());
	vector<int> dp;
	dp.push_back(1);
	for (int i = 1; i <= tar; i++) {
		int tres = 0;
		for (int j = 0; j < cash.size(); j++) {
			if (cash[j] > i)break;
			tres += dp[i - cash[j]];
		}
		dp.push_back(tres);
	}
	int index = 0;
	for (auto i : dp) { cout << "dp: "<<index++ << "\t"; }cout << endl<<endl;
	for (auto i : dp) { cout << i << "\t"; }cout << endl;
}

问题7:打家劫舍

题目描述:你是一个专业的强盗,计划抢劫沿街的房屋。每间房都藏有一定的现金,阻止你抢劫他们的唯一的制约因素就是相邻的房屋有保安系统连接,如果两间相邻的房屋在同一晚上被闯入,它会自动联系警方。
houses={ 1,5,3,9,2,5,10 };
dp={0,0,0,0,0,0,0,0}//vector dp(houses.size()+1,0);
当只有一个家庭时,显然必须偷啊:dp[1]=houses[0]
当只有两个家庭时就要考虑考虑了:
dp[2]=max(houses[0],houses[1])=max(dp[1],dp[0]+houses[1])
依此类推:
dp[i]=max(dp[i-1],dp[i-2]+houses[i])
其中注意的是第一项dp[i-1]代表了不偷当前房屋的子解,
而第二项dp[i-2]+houses[i]代表了偷当前房屋时的总价值.
理解了这个公式之后就好做了:

void steal(vector<int> houses) {
	int hl = houses.size();
	vector<int> dp(hl+1,0);
	dp[1]=houses[0];
	for (int i = 2; i <= hl; i++) {
		dp[i] = max(dp[i-1],dp[i-2]+houses[i-1]);
	}
	int index = 0;
	for (auto i : dp) { cout << "dp: "<<index++ << "\t"; }cout << endl<<endl;
	for (auto i : dp) { cout << i << "\t"; }cout << endl;
}

如图为dp表:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值