【背包】
今天来讲讲背包问题
背包问题是DP问题的一种。
而DP问题离不开三个特性:
- 最优子结构
- 无后效性
- 重叠子问题
在讲背包问题之前,我们先讲讲斐波那契数列。
总所周知斐波那契数列的递推公式是
F(n)=F(n-1)+F(n-2)
但是我们可以画个图看看,在使用递归求解斐波那契数列第N项的时候会发生什么问题
写到这里应该就很清楚了。我们要求F(n)一定要递归求得F(n-1)和F(n-2),但是我们在求F(n-1)的时候,我还需要再递归求一个F(n-2),那么时间的开销就变大了许多许多。这就是上面DP说的重叠子问题,我重复地求了一个值,以至于当n很大很大地时候接近叶子节点的数据被计算了相当多次,这时间的开销是非常非常大的。而且我们从时间复杂度的角度出发,这个算法本身也是O(2^n)。
那么。我们从节省时间的角度我们去解决一下斐波那契数列。既然我们重复地计算了某一个值,那我们能不能用一个变量先存放他在那。直到用的时候,我才把他调度出来。也就是说,我们从n=2开始我们用for循环递进的方式,一个一个地求出前n-1项地值,那么我们在计算第n项地值得时候,我们就可以之间从数组中通过下标索引得方式去得到F(n-1)和F(n-2)从而计算出F(n)的值。
这里我就不进行代码实现了。
我们回归到今天的主角:背包问题
有N件物品和一个容量为V的背包。每种物品均只有一件。 第i件物品的质量是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的质量总和不超过背包容量,且价值总和最大。
在本题中,乍一看,好像没有思路,或者有的同学会觉得性价比方案可以解决背包问题
我们一步步来仿照刚刚的斐波那契数列的O(n)求解法
第一步
我们先完成简单的输入,把缓冲区的数据全部读进一个数组中,并按照价值,升序排序
struct object {
int val;
int weight;
};
//这里是存储的数据类型
int main() {
int T;
//T代表着我有多少个物体
while (cin >> T) {
int weight = 0;
cin >> weight;
object* Arr = new object[T];
for (int count = 0; count < T; count++) cin >> Arr[count].val >> Arr[count].weight;
//input
//sort
for (int count = 0; count < T; count++) {
for (int j = count+1; j < T; j++) {
if (Arr[count].val > Arr[j].val) {
//change
object tem = Arr[count];
Arr[count] = Arr[j];
Arr[j] = tem;
}
}
}
}
}
这里用了冒泡排序增强可读性,我们也可以用其他更快速的排序去减少时间的复杂度,不过这不属于我们的核心代码块,而且排序不是必须的。因为每次选择我均保证最优,所以不一定要排序。
我们这里直接定义一个函数名为DP_bag
图解如下
于是我们便有递归实现的代码
int DP_bag(object *Arr,int Value,int weight,int last){
if (last < 0)return 0;//装不下了
else if (weight < Arr[last].weight)return DP_bag(Arr, Value, weight, last - 1);//装不下最贵的,往后找
else return max(DP_bag(Arr, Value, weight, last - 1), Arr[last].val +DP_bag(Arr, Arr[last].val + Value, weight - Arr[last].weight, last - 1));
}
找到最优子结构和重叠子问题后就可转换
我们先设置一个Dp数组,为了避免多次计算重叠的子问题,我们可以用一个DP数组来存储每一个值
int DP_bag(object* Arr, int T,int weight) {
int* DP = new int[weight+1];
memset(DP, 0, weight+1);//T3RSCG
for (int i = 0; i < T; i++) {
for (int j = weight; j >= Arr[i].weight; j--) {
DP[j] = max(DP[j], DP[j - Arr[i].weight] + Arr[i].val);
}
}
return DP[weight];
}
我们来解剖一下这段代码,在DP数组中,我们一开始利用memset函数将他们归0,然后是一个很难理解的嵌套式循环。
1.在这个嵌套循环中,我们先看这两个循环的判断条件。
i < T
j >= Arr[i].weight
(1)在第一层循环,很简单,我能保证每一次的i都指向当前物体,所以我们有当i<T的时候我们会退出最外层的循环,这样就能保证每一次的Arr[i]均表示当前的物体(第i个物体)。
(2)在第二层循环中我们的退出条件是,当J小于Arr[i].weight我们就退出,因为Arr[i]表示当前的物体,当J小于(这里的J表示当背包最大承重为J时),如果小于表示,我已经装不下这个物体了,那我就退出
2.我们看循环内部的最优子结构
DP[j] = max(DP[j], DP[j - Arr[i].weight] + Arr[i].val);
在这个"方程"中,我们可以看到,当访问到DP[j]的时候(DP[j]的意思表示成,当背包承重为j时候所承担的最大的价值),那我此时已经知道了DP[j]前面的值(即DP[0],DP[1],DP[2]…DP[j-1])
综合来看,我们可以理解成,每输入一个物品(外层循环每跑一次),我就更新一下DP数组的值,那如何更新?
在内层循环中,我其实做的就是,在原有的DP数组的基础上,输入一个物体,看能不能装的下,能装的上,那我就比较更新当前总承重(变成总承重-该物体重量)后的值(DP[j-该物体的重量])+该物体的价值的总价值大,还是不更新的值(DP[j])大,那么更新到最后我就能保证DP[n]是最优解(由于前面的每一个承重都能保证最优解)
总结一下把,该类的背包问题其实是属于01背包问题
在以二进制为主导数制的计算机中,0往往代表着低电平,1代表着高电平。
回归到该问题,我们无非离不开判断(放/不放),也就是1 与 0之间的关系。
01背包问题,也就因此得名