1.1选题简介
假设有一个能装入总体积为T的背包和n件体积分别为w1,w2,…wn的物品,能否从n件物品中挑选若干件恰好装满背包,即使w1+w2+…+wm=T,要求找出所有满足上述条件的解。若每件物品同时具有体积和价值,背包有大小限制,求物品总体积最大值-------最优或近似最优解
1.2问题分析
可利用回溯法的设计思想来解决背包问题。首先,将物品排成一列,然后,顺序选取物品装入背包,若已选取第i件物品后未满,则继续选取第i+1件,若该件物品“太大”不能装入,则弃之,继续选取下一件,直至背包装满为止。
如果在剩余的物品中找不到合适的物品以填满背包,则说明“刚刚”装入的物品“不合适”,应将它取出“弃之一边”,继续再从“它之后”的物品中选取,如此重复,直到求得满足条件的解,或者无解。
由于回溯求解的规则是“后进先出”,自然要用到“栈”。
1.3数据结构和算法设计
1.3.1数据结构
使用栈,以顺序结构实现。PS:成员函数的实现较为冗长,故未给出
// Stack.h 用于存放常量,函数的声明部分,类的声明部分
#ifndef HEAD_H
#define HEAD_H 1
template <class Elem>
class Stack
{
public:
Stack(int size);
~Stack() { delete[] data; }
bool IsEmpty() const;
bool IsFull();
Elem Pop();
void Push(Elem x);
inline Elem Top() { return data[top - 1]; }
int Size() { return top; }
private:
int maxsize;
int top;
Elem *data;
};
#endif
1.3.2算法设计
①利用栈的特性:后进先出,达到一种回溯的效果,来计算出刚好装入物品的体积为背包体积的各种方案
但是如果考虑每件商品的价值,在计算最优价值的时候,若使用栈把每一次的方案的价值记录下来并比较时间复杂度可能达到O(2^n),因此在计算价值时采取了dp动态规划的思想,将时间复杂度改进到O(n^2)。
②动态规划,对于任意一个物品有两种状态,放入和不放入。
放入:剩余重量=当前包的重量-放入的该物品重量,拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化后,再加上本物品的价值。也就是,放入的最大总价值=剩余重量的最大价值+本物品的最大价值。
不放入:剩余重量=当前包的重量,拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化。不放入的最大总价值=剩余重量的最大值。
这边:拿着剩余重量,从剩余物品中选取来尽可能使总价值最大化。其实就是这里的子问题,动规dp的精髓就是,记录下子问题的最优解。
因为每个物体,都有装与不装两种选择,所以我们得到状态转移方程:
f [ j ] =max(f [ j ], f [ j-w [ i ]] +w [ i ]);
f [ j ] 为:当总容量为 j 时,不放第 i 件物品,所能装的最大体积。
f [ j-w [ i ]] +w [ i ] 为:当总容量为 j 时,放了第 i 件物品后,所能装的最大体积。(即 j 减去第 i 件物品体积 的容量能装的最大体积+第 i 件物品的体积。 w [ i ] 为第 i 件物品体积)
1.4实验过程
1.4.1开发环境
本次实验中使用的环境配置如下:
(1)编译器及其版本:MinGW-w64
(2)编辑器:Visual Studio Code
1.4.2核心代码
void KnapSack(int N, int V, int *BagV, int *BagValue)
{
cout << "无价值,仅考虑体积:\n";
int V_sum = V; // 实时记录体积和
int count = 0; // 记录解的个数
Stack<int> st(N);
int i = 1, k = 0;
while (i < N)
{
if (V_sum >= BagV[k]) // 先放入一个到栈中,要判断是否符合体积
{
st.Push(k);
V_sum -= BagV[k];
break;
}
else
{
i++, k++;
}; // i记录下一个目标,k记录第一个放入的
}
if (i == N)
{
cout << "No answer";
return;
}
while (1) // 这里类似于二叉树利用堆栈实现先序遍历
{
while (V_sum > 0 && i < N)
{
if (V_sum >= BagV[i])
{
V_sum -= BagV[i];
st.Push(i);
}
if (V_sum == 0)
{
count++;
break;
}
i++;
}
if (V_sum == 0)
Output(st, count, N, BagV, BagValue); // 找到合适的一组解并输出
if (!st.IsEmpty())
{
if (st.Size() == 1) // 排除i==N-1的情况,以及判断是否筛选完成
{
i = st.Pop() + 1;
V_sum += BagV[i - 1];
if (i == N)
break;
}
else if (i == N - 1)
{
V_sum += BagV[st.Pop()];
i = st.Top() + 1;
V_sum += BagV[st.Pop()];
}
else
{
i = st.Pop() + 1;
V_sum += BagV[i - 1];
}
}
}
if (!count)
cout << "No anwser";
else
cout << "总方案数:" << count << endl;
}
void Advance_KnapSack(const int N, const int V, int *BagV, int *BagValue)
{
for (int i = 1; i <= N; i++) // 只装入0件物品,价值一定为0
{
for (int j = 1; j <= V; j++) // 背包体积为0,价值一定为0
{
if (j < BagV[i - 1])
dp[i][j] = dp[i - 1][j]; // 当前物品不能装入
else // 可以装入
{
dp[i][j] = Max(dp[i - 1][j - BagV[i - 1]] + BagValue[i - 1], dp[i - 1][j]);
}
}
}
Output(N, V, BagV);
}
1.5实验结果与分析
1.5.1程序测试
完成代码编写后,编译运行程序,并做如下的测试:
表1.1 XXXX测试内容
项目 | 测试 | 程序输出 | 测试结果 |
1 | (1,1)(8,8)(4,4)(3,3)(5,5)(2,2) | 有4种方案,最优价值为10,详细结果见下图 | 通过 |
2 | (2,4)(4,6)(1,2)(3,3) | 两种方案,最优价值为 10 | 通过 |
3 | (6,6) | 超过背包体积,无解 | 通过 |
4 | (3,8)(1,10) | 体积未装满,但有近似最优解 | 通过 |
1.5.2结果分析
我们前两组测试用例满足体积刚好为V有多种情况,并依次输出其方案和最大的价值及对应的物品体积
第三组数据没有一件物品能够装得下,因此程序输出No answer;
第四组数据无法满足体积刚好为V的情况,但价值上有近似最优解(总体积小于背包体积)
经过多组用例验证,程序符合实验要求,完全达到预期的实验目的。
1.6心得与体会
经过该次实验,我更加深刻的理解了数据结构:栈的原理及设计使用,并且学会计算算法的时间及空间复杂度来判断程序算法设计的优劣,以及为了进一步优化算法,采用了dp动态规划,将算法的时间复杂度从O(2^n)优化到了O(n^2),大大减小了时间开销,优化进程。