首先从最简单的01背包入手来理解背包问题,再进行延申解决完全背包、多重背包的问题,最长公共子序列的问题。我的代码有挺多多余的部分,主要是为了配合解析的思路写的。
问题1:01背包
我们有一个容量为x 的背包,有重量为数组w和对应价值为数组v的物件,解决如何使背包里的物件总价值最高的问题。
为了简单理解,假设只有三个物件:w={2,4,3};v={3,7,5};
我们的背包容量假设为8;
我们尝试用三个管理员来对应地管理这三个物件:
管理员0说:我就只看这个背包装第1个物件是啥情况,啊因为一个物件只能放一次,所以容量大于2之后就是这个样子的嘛。。
背包重量 | 背包价值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|
2 | 3 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
管理员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对应的是当前背包容量。)
背包重量 | 背包价值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|
4 | 7 | 0 | 0 | 3 | 3 | 7 | 7 | 10 | 10 | 10 |
管理员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表: