文章目录
一、实验原理
1.1 背包问题
背包问题已经是一个很经典而且讨论很广泛的算法问题了。
背包问题泛指这类种问题:给定一组有固定价值和固定重量的物品,以及一个已知最大承重量的背包,求在不超过背包最大承重量的前提下,能放进背包里面的物品的最大总价值。具体各类背包问题可以分成以下 3 种不同的子问题。
1.1.1 0-1背包问题
问题描述:有编号分别为 a,b,c,d,e 的五件物品,它们的重量分别是 2,2,6,5,4,它们的价值分别是 6,3,5,4,6,每件物品数量只有一个,现在给你个承重为 10 的背包,如何让背包里装入的物品具有最大的价值总和?
特点:每个物品只有一件,选择放或者不放
1.1.2 完全背包问题
问题描述:有编号分别为 a,b,c,d 的四件物品,它们的重量分别是 2,3,4,7,它们的价值分别是 1,3,5,9,每件物品数量无限个,现在给你个承重为 10 的背包,如何让背包里装入的物品具有最大的价值总和?
特点:每个物品可以无限选用
1.1.3 多重背包问题
问题描述:有编号分别为 a,b,c 的三件物品,它们的重量分别是 1,2,2,它们的价值分别是 6,10,20,他们的数目分别是 10,5,2,现在给你个承重为 8 的背包,如何让背包里装入的物品具有最大的价值总和?
特点 :每个物品都有一定的数量
1.2 解决算法
1.2.1 动态规划算法
动态规划原理:动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。
动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。用动态规划方法解决 0-1 背包问题:
步骤 1-找子问题:子问题必然是和物品有关的,对于每一个物品,有两种结果:能装下或者不能装下。
第一,包的容量比物品体积小,装不下,这时的最大价值和前 i-1 个物品的最大价值是一样的。
第二,还有足够的容量装下该物品,但是装了不一定大于当前相同体积的最优价值,所以要进行比较。由上述分析,子问题中物品数和背包容量都应当作为变量。因此子问题确定为背包容量为 j 时,求前 i 个物品所能达到最大价值。
步骤 2-确定状态:由上述分析,“状态”对应的“值”即为背包容量为 j 时,求前 i个物品所能达到最大价值,设为 dp[i][j]。初始时,dp[0][j] (0<=j<=V)为 0,没有物品也就没有价值。
步骤 3-确定状态转移方程:由上述分析,第 i 个物品的体积为 w,价值为 v,则状态转移方程为:
1.2.2 贪婪算法
贪心法把一个复杂问题分解为一系列较为简单的局部最优选择,每一步选择都是对当前的一个扩展,直到获得问题的完整解。
k-optimal 算法用来解决 0-1 背包问题:
步骤 1-计算每种物品单位重量的价值 Vi/Wi ;
步骤 2-依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。
步骤 3-若将这种物品全部装入背包后,背包内的物品总重量未超过 C,则选择单位重量价值次高的物品并尽可能多地装入背包。
依此策略一直地进行下去,直到背包装满为止。
1.2.3回溯法
回溯法先确定解空间的结构,使用深度优先搜索,搜索路径一般沿树形结构进行,在搜索过程中,首先会判断所搜索的树结点是否包含问题的解,如果肯定不包含,则不再搜索以该结点为根的树结点,而向其祖先结点回溯;否则进入该子树,继续按深度优先策略搜索。
运用回溯法解题通常包含以下三个步骤:
a. 针对所给问题,定义问题的解空间;
b. 确定易于搜索的解空间结构;
c. 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索;
1.2.4分支界定法
分支限界法类似于回溯法,也是在问题的解空间上搜索问题解的算法。
分支限界法首先要确定一个合理的限界函数(bound funciton),并根据限界函数确定目标函数的界[down ,up],按照广度优先策略或以最小耗费优先搜索问题的解空间树,在分直结点上依次扩展该结点的孩子结点,分别估算孩子结点的目标函数可能值,如果某孩子结点的目标函数可能超出目标函数的界,则将其丢弃;否则将其加入待处理结点表(简称 PT表),依次从表 PT 中选取使目标函数取得极值的结点成为当前扩展结点,重复上述过程,直到得到最优解。
常见的两种分枝限界法包含队列式(FIFO)分支限界法和优先队列式分支限界法。
二、实验要求
算法设计:
输入物品数 n,背包容量 c,输入 n 个物品的重量、价值,在以上算法中任选两个实现最优解决 0-1 背包问题。
请问:所选算法的实现流程图或者伪代码是什么?比较时间复杂度和空间复杂度,得出什么结论
选择第一个算法动态规划和第二个算法贪婪算法来实现。
2.1动态规划实现
根据前面描述的动态规划原理和状态转移方程来解决0-1背包问题。
代码如下:
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n,capacity;
cout << "请输入物品的数量" << endl;
cin >> n;
cout << "请输入背包的容量" << endl;
cin >> capacity;
vector<int> weight(n + 1,0);
vector<int> value(n + 1,0);
cout << "请输入物品的重量" << endl;
for(int i = 1;i <= n;i++){
cin >> weight[i];
}
cout << "请输入物品的价值" << endl;
for(int i = 1;i <= n;i++){
cin >> value[i];
}
vector<vector<int>> dp(n + 1,vector<int>(capacity + 1,0));
for(int i = 1;i <= n;i++){
for(int j = 1;j <= capacity;j++){
if(weight[i] <= j){
dp[i][j] = max(dp[i-1][j - weight[i]] + value[i],dp[i-1][j]);
}
else{
dp[i][j] - dp[i-1][j];
}
}
}
cout << "最大价值总和为" << dp[n][capacity] << endl;
}
程序输出结果为:
输出结果正确,该代码时间复杂度O(n * capacity),空间复杂度也为O(n * capacity),该代码可以简化一下空间复杂度,因为选择与否一个物体是根据前一个物体的选择来进行状态转移的,因此可以采用滚动数组的方式来优化空间复杂度,可降为O(capacity)级别的空间复杂度滚动数组注意遍历顺序
。
滚动数组代码如下所示:
vector<int> dp1(capacity + 1,0);
for(int i = 1;i <= n;i++){
for(int j = capacity; j >= weight[i];j--){
dp1[j] = max(dp1[j - weight[i]] + value[i],dp1[j]);
}
}
cout << "最大价值总和1为" << dp1[capacity] << endl;
2.2贪婪算法实现
根据算法思想,将物品按v/w进行排序,每次取出权值最大的物品看能否放入背包中,若能放入则放入,否则放入次大的物品。
代码如下所示:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main(){
int n,capacity;
cout << "请输入物品的数量" << endl;
cin >> n;
cout << "请输入背包的容量" << endl;
cin >> capacity;
vector<int> weight(n + 1,0);
vector<int> value(n + 1,0);
int result = 0;
cout << "请输入物品的重量" << endl;
for(int i = 1;i <= n;i++){
cin >> weight[i];
}
cout << "请输入物品的价值" << endl;
for(int i = 1;i <= n;i++){
cin >> value[i];
}
先按v/w的大小进行排序
for(int i = 1;i < n;i++){
for(int j = 1;j < n;j++){
// cout << (double)value[j] / (double)weight[j] << " " << (double)value[j + 1]/(double)weight[j + 1]<< endl;
if((double)value[j] / (double)weight[j] < (double)value[j + 1]/(double)weight[j + 1]){
swap(weight[j],weight[j+1]);
swap(value[j],value[j+1]);
}
}
}
for(int i = 1;i <= n;i++){
if(capacity >= weight[i]){
result += value[i];
capacity -= weight[i];
}
}
cout << "最大价值总和为" << result << endl;
}
本算法的时间复杂度主要在排序上,采用了冒泡排序的方式,因此时间复杂度为O(n^2),空间复杂度为O(1)。n为物品的数量
为了优化该算法的时间复杂度,采用了优先队列自定义排序规则的方式来实现,代码如下所示:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
struct cmp{
bool operator()(pair<int,int>& a, pair<int,int>& b){
return (double)a.second/(double)a.first < (double)b.second/(double)b.first;
}
};
int main(){
int n,capacity;
cout << "请输入物品的数量" << endl;
cin >> n;
cout << "请输入背包的容量" << endl;
cin >> capacity;
vector<int> weight(n + 1,0);
vector<int> value(n + 1,0);
int result = 0;
cout << "请输入物品的重量" << endl;
for(int i = 1;i <= n;i++){
cin >> weight[i];
}
cout << "请输入物品的价值" << endl;
for(int i = 1;i <= n;i++){
cin >> value[i];
}
priority_queue<int,vector<pair<int,int>>,cmp> queue;
for(int i = 1;i <= n;i++){
queue.push(make_pair(weight[i],value[i]));
}
while(!queue.empty()){
cout << queue.top().first << " " << queue.top().second << endl;
if(capacity >= queue.top().first){
result += queue.top().second;
capacity -= queue.top().first;
}
queue.pop();
}
cout << "最大价值总和为" << result << endl;
}
该方法时间复杂度为O(nlogn),空间复杂度为O(n),至此贪婪算法的实现也结束。
程序输出截图如下图所示:
2.3 结论
动态规划算法由于考虑到了所有种可能的情况并存储,因此相较而言时间和空间复杂度肯定会高一下。(方法二如果采用快排重定义排序规则的方式,会进一步降低空间复杂度到O(logn)),因此虽然动规方法很好,但对于性能要求高的需求来说可以适当采用其它方法。
总结
本实验主要针对0-1背包的问题,采用不同种算法思想来解决。本实验介绍了四种方法,我主要采用了第一种动态规划和第二种贪婪算法来实现,提升了对该问题以及有关算法的了解程度。