文章目录
概述
背包问题已经被研究了超过一个世纪,是动态规划中的经典问题,也是理解动态规划思想的重要工具。背包问题可以被抽象解释为:给出一个特定体积(重量)容量的背包,再分别给出多个物品的体积(重量)和价值,求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
0/1背包问题
问题描述
有体积容量为 M M M的背包和 N N N件物品,第 i ( 0 ≤ i < N ) i(0\leq i < N) i(0≤i<N)件物品的体积为 w w w、价值为 c c c,求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入格式
每个测试包含一个测试用例,第一行分别给出背包的体积容量 M M M和物品的件数 N N N,接下来有 N N N行数据,每行分别给出第 i i i件物品的体积 w w w和价值 c c c。
输出格式
输出一行数据,在此行内给出背包内物品的价值总和的最大值。
输入样例
200 8
79 83
58 14
86 54
11 79
28 72
62 52
15 48
68 62
输出样例
334
动态规划
解题思路
需要解决的根本问题就是需不需要把当前物品放入背包,所以被称为0/1背包问题。
我们暂且将最终得到的结果称为最优背包,最终的最优背包可以看成是比其体积容量小的最优背包和一件物品组合而成。由于比其容量小的最优背包有多种,物品有多种,所以它们之间的组合方式也有很多种,我们只需要在多种组合方式种找到价值最大的组合方式。我们求出最优背包就能求出比其体积容量更大的最优背包,而这也就满足了最优子结构性质,背包问题也就可以通过动态规划解决。
在小体积容量最优背包与物品组合的过程中需要注意不能将最优背包内已经含有的物品再次与最优背包进行组合。
根据以上的思想就能得到动态规划的转移方程:
i
for
0
to
N
−
1
j
for
m
to
w
[
i
]
f
[
j
]
=
max
(
f
[
j
−
w
[
i
]
]
+
c
[
i
]
,
f
[
j
]
)
i \quad \textrm{for} \quad 0 \quad \textrm{to} \quad N-1 \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \\ j \quad \textrm{for} \quad m \quad \textrm{to} \quad w[i] \quad \quad \quad \quad \quad \quad \quad \quad \\ f[j] = \max\left(f[j-w[i]]+c[i],f[j]\right)
ifor0toN−1jformtow[i]f[j]=max(f[j−w[i]]+c[i],f[j])
其中
f
[
j
]
f[j]
f[j]表示体积容量为
j
j
j的最优背包的价值,
w
[
i
]
w[i]
w[i]表示第
i
i
i件物品的体积,
c
[
i
]
c[i]
c[i]表示第
i
i
i件物品的价值,
m
m
m表示背包的体积容量。
测试代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
int main(){
size_t m,n;
cin >> m >> n;///输入背包的体积容量和物品件数
vector<size_t> w(n);
vector<size_t> c(n);
for(size_t i=0;i<n;i++){
cin >> w[i] >> c[i];///输入每件物品的体积和价值
}
vector<size_t> f(m+1,0);///申请内存空间,将各体积容量的背包初始化价值为0
for(size_t i=0;i<n;i++){///枚举出各件物品
for(size_t j=m;j>=w[i];j--){///利用小体积容量的最优背包求大体积容量的最优背包
f[j] = max(f[j-w[i]]+c[i],f[j]);///状态转移方程
}
}///第一重循环枚举出各件物品,第二重循环枚举出各体积容量的最优背包,转移方程找出最佳的物品和最优背包组合
cout << f[m] << endl;
return 0;
}
回溯思想
解题思路
测试代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
struct Stone{
size_t weight;
size_t price;
float purity;
Stone(size_t weight,size_t price){
this->weight = weight;
this->price = price;
this->purity = (float)price/weight;
}
inline bool operator<(Stone &other)const{
return this->purity<other.purity;
}
};
class KnapProblem{
size_t capacity;
size_t number;
vector<Stone> stones;
size_t weight;
size_t price;
size_t maximum;
vector<bool> content;
public:
KnapProblem(){
cin >> this->capacity >> this->number;
this->content.resize(number,false);
for(size_t i=0;i<this->number;i++){
size_t weight,price;
cin >> weight >> price;
this->stones.push_back(Stone(weight,price));
}
sort(this->stones.rbegin(),this->stones.rend());
this->weight = 0;
this->price = 0;
this->maximum = 0;
}
bool bound(size_t index){
size_t surplus = this->capacity-this->weight;
size_t prediction = this->price;
while(++index<this->number&&this->stones[index].weight<=surplus){
surplus -= this->stones[index].weight;
prediction += this->stones[index].price;
}
if(index<this->number){
prediction += surplus*this->stones[index].purity;
}
if(prediction>this->maximum){
return true;
}
return false;
}
void backtrack(size_t index){
if(index==this->number){
this->maximum = this->price;
}else{
if(this->weight+this->stones[index].weight<=this->capacity){
this->weight += this->stones[index].weight;
this->price += this->stones[index].price;
this->content[index] = true;
backtrack(index+1);
this->weight -= this->stones[index].weight;
this->price -= this->stones[index].price;
}
if(bound(index)){
this->content[index] = false;
backtrack(index+1);
}
}
}
void print(){
cout << this->maximum << endl;
for(size_t i=0;i<this->number;i++){
if(this->content[i]){
cout << this->stones[i].weight << ' ' << this->stones[i].price << endl;
}
}
}
};
int main(){
KnapProblem kp;
kp.backtrack(0);
kp.print();
return 0;
}
分支限界
解题思路
测试代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
struct Stone{
size_t weight;
size_t price;
float purity;
Stone(size_t weight,size_t price){
this->weight = weight;
this->price = price;
this->purity = (float)price/weight;
}
inline bool operator<(Stone &other)const{
return this->purity<other.purity;
}
};
struct Knap{
size_t weight;
size_t price;
size_t index;
size_t prediction;
Knap *parent;
Knap(){
this->weight = 0;
this->price = 0;
this->index = 0;
this->parent = nullptr;
}
Knap(Knap *parent){
this->weight = parent->weight;
this->price = parent->price;
this->index = parent->index+1;
this->parent = parent;
}
Knap(Knap *parent,Stone &stone){
this->weight = parent->weight+stone.weight;
this->price = parent->price+stone.price;
this->index = parent->index+1;
this->parent = parent;
}
};
struct KnapPointerCompare{
inline bool operator()(const Knap *left,const Knap *right)const{
return left->prediction<right->prediction;
}
};
class KnapProblem{
size_t capacity;
size_t number;
vector<Stone> stones;
priority_queue<Knap *,vector<Knap *>,KnapPointerCompare> open;
vector<Knap *> closed;
public:
KnapProblem(){
cin >> this->capacity >> this->number;
for(size_t i=0;i<this->number;i++){
size_t weight,price;
cin >> weight >> price;
this->stones.push_back(Stone(weight,price));
}
sort(this->stones.rbegin(),this->stones.rend());
}
void predict(Knap *knap){
size_t surplus = this->capacity-knap->weight;
size_t prediction = knap->price;
size_t index = knap->index;
while(index<this->number&&this->stones[index].weight<=surplus){
surplus -= this->stones[index].weight;
prediction += this->stones[index].price;
index++;
}
if(index<this->number){
prediction += surplus*this->stones[index].purity;
}
knap->prediction = prediction;
}
Knap *branchAndBound(){
Knap *parent,*child;
parent = new Knap();
predict(parent);
this->open.push(parent);
while(!this->open.empty()){
parent = this->open.top();
this->open.pop();
this->closed.push_back(parent);
if(parent->index==this->number){
return parent;
}else{
if(parent->weight+this->stones[parent->index].weight<=this->capacity){
child = new Knap(parent,this->stones[parent->index]);
predict(child);
this->open.push(child);
}
child = new Knap(parent);
predict(child);
this->open.push(child);
}
}
return nullptr;
}
~KnapProblem(){
while(!this->open.empty()){
delete this->open.top();
this->open.pop();
}
for(Knap *knap:this->closed){
delete knap;
}
}
void print(Knap *child){
cout << child->price << endl;
for(Knap *parent=child->parent;parent;child=parent,parent=child->parent){
if(child->weight-parent->weight){
cout << child->weight-parent->weight << ' ' << child->price-parent->price << endl;
}
}
}
};
int main(){
KnapProblem kp;
kp.print(kp.branchAndBound());
return 0;
}
完全背包问题
问题描述
有体积容量为 M M M的背包和 N N N种物品,第 i ( 0 ≤ i < N ) i(0\leq i < N) i(0≤i<N)种物品的体积为 w w w、价值为 c c c,每种物品的数量不限,求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入格式
每个测试包含一个测试用例,第一行分别给出背包的体积容量 M M M和物品的种数 N N N,接下来有 N N N行数据,每行分别给出第 i i i种物品的体积 w w w和价值 c c c。
输出格式
输出一行数据,在此行内给出背包内物品的价值总和的最大值。
输入样例
200 8
79 83
58 14
86 54
11 79
28 72
62 52
15 48
68 62
输出样例
1422
解题思路
在0/1背包问题中不可以将最优背包内已经含有的物品再次与最优背包进行组合,而在完全背包问题中,由于每种物品的数量没有限制,需要将最优背包内已含有的物品再次与最优背包进行组合。也就是只需要在0/1背包问题的代码基础上将枚举最优背包的顺序进行翻转。
根据以上的思想就能得到动态规划的转移方程:
i
for
0
to
N
−
1
j
for
w
[
i
]
to
m
f
[
j
]
=
max
(
f
[
j
−
w
[
i
]
]
+
c
[
i
]
,
f
[
j
]
)
i \quad \textrm{for} \quad 0 \quad \textrm{to} \quad N-1 \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \\ j \quad \textrm{for} \quad w[i] \quad \textrm{to} \quad m \quad \quad \quad \quad \quad \quad \quad \quad \\ f[j] = \max\left(f[j-w[i]]+c[i],f[j]\right)
ifor0toN−1jforw[i]tomf[j]=max(f[j−w[i]]+c[i],f[j])
其中
f
[
j
]
f[j]
f[j]表示体积容量为
j
j
j的最优背包的价值,
w
[
i
]
w[i]
w[i]表示第
i
i
i件物品的体积,
c
[
i
]
c[i]
c[i]表示第
i
i
i件物品的价值,
m
m
m表示背包的体积容量。
测试代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int main(){
size_t m,n;
cin >> m >> n;///输入背包的体积容量和物品种类数
vector<size_t> w(n);
vector<size_t> c(n);
for(size_t i=0;i<n;i++){
cin >> w[i] >> c[i];///输入每种物品的体积和价值
}
vector<size_t> f(m+1,0);///申请内存空间,将各体积容量的背包初始化价值为0
for(size_t i=0;i<n;i++){///枚举出各种物品
for(size_t j=w[i];j<=m;j++){///利用小体积容量的最优背包求大体积容量的最优背包
f[j] = max(f[j-w[i]]+c[i],f[j]);///状态转移方程
}
}///第一重循环枚举出各种物品,第二重循环枚举出各体积容量的最优背包,转移方程找出最佳的物品和最优背包组合
cout << f[m] << endl;
return 0;
}
多重背包问题
问题描述
有体积容量为 M M M的背包和 N N N种物品,第 i ( 0 ≤ i < N ) i(0\leq i < N) i(0≤i<N)件物品的体积为 w w w、价值为 c c c,数量为 q q q,求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入格式
每个测试包含一个测试用例,第一行分别给出背包的体积容量 M M M和物品的件数 N N N,接下来有 N N N行数据,每行分别给出第 i i i种物品的体积 w w w、价值 c c c和数量 q q q。
输出格式
输出一行数据,在此行内给出背包内物品的价值总和的最大值。
输入样例
200 8
79 83 2
58 14 3
86 54 5
11 79 1
28 72 8
62 52 4
15 48 10
68 62 9
输出样例
631
解题思路
了解了0/1背包问题后,最朴素的想法就是将每件物品全部当作独立的个体,不关心它的种类,然后运用0/1背包解决问题。根据以上思路,问题就转换成了
∑
i
=
0
N
−
1
q
i
\sum_{i=0}^{N-1}q_i
∑i=0N−1qi(
q
i
q_i
qi为第
i
i
i种物品的件数)件物品的0/1背包问题。
忽略物品的种类无疑是对已知条件的一种浪费,可以对种类加以利用来减少算法的时间复杂度。我们可以对同一种类的物品进行内部合并,将多件物品合并成一件物品,这样就可以减少物品的总数量,以降低算法的时间复杂度。但是我们需要找到一种恰当的合并方法,假如某类物品有13件,合并后的物品之间进行组合要可以表示1到13件原物品。
根据二进制思想,二进制数可以表示任意的十进制正整数。我们可以把件数拆分出几个2的指数次幂,这样就可以表示任意的件数。任何整数都可以表示成:
Q
=
(
∑
i
=
0
⌊
log
2
(
Q
+
1
)
⌋
−
1
2
i
)
+
R
Q=\left(\sum_{i=0}^{\lfloor\log_2(Q+1)\rfloor-1}2^i\right)+R
Q=
i=0∑⌊log2(Q+1)⌋−12i
+R
例如13就可以划分成1、2、4和6,1、2、4为2的指数次幂,6为余项
R
R
R,这四个数进行组合就可以表示1到13。通过上面的例子可知13件同类物品可以用4件物品来替代,这4件物品体积和价值分别相当于原物品的1件、2件、4件和6件。
我们对每种物品的数量都利用上式来划分,若有
Q
Q
Q件原物品,那么划分后就变成了
log
2
Q
\log_2Q
log2Q件物品。利用以上思想,原问题就转换成了
∑
i
=
0
N
−
1
log
2
q
i
\sum_{i=0}^{N-1}\log_2q_i
∑i=0N−1log2qi(
q
i
q_i
qi为第
i
i
i种物品的件数)件物品的0/1背包问题。
测试代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
int main(){
size_t m,n;
cin >> m >> n;///输入背包的体积容量和物品种类数
vector<size_t> w,c;
for(size_t i=0;i<n;i++){
size_t tw,tc,q;
cin >> tw >> tc >> q;///输入物品的体积、价值和数量
for(size_t j=1;q>j;j*=2){///将q件物品组合成log q件物品
w.push_back(tw*j);
c.push_back(tc*j);
q -= j;
}
w.push_back(tw*q);
c.push_back(tc*q);
}///将物品组合后,多重背包问题被改造成0/1背包问题
vector<size_t> f(m+1,0);///申请内存空间,将各体积容量的背包初始化价值为0
for(size_t i=0;i<w.size();i++){///枚举出各件物品
for(size_t j=m;j>=w[i];j--){///利用小体积容量的最优背包求大体积容量的最优背包
f[j] = max(f[j-w[i]]+c[i],f[j]);///状态转移方程
}
}///第一重循环枚举出各件物品,第二重循环枚举出各体积容量的最优背包,转移方程找出最佳的物品和最优背包组合
cout << f[m] << endl;
return 0;
}