变形问题:2N长度数组,分成俩N长度的,使其和差值最小。【程序员面试100题之十五:数组分割】
(写在前面)背包问题 总结的要点:
(黄色字体标题的代码为三个问题最终用的模板)
1.一维(矩阵)的01背包为啥要逆序(保证更新f[j]时,f[ j - weight[i] ]是没有放入物品i时的数据即f[ i - 1 ][ j - weight[i] ],因为01背包每个物品至多被选一次。而完全背包中,每个物品可以被选无限次,那么状态f[i][j],正好可以由可能已经放入物品i的状态f[ i - 1 ][ j - weight[i] ]转移而来。所以,遍历顺序改为顺序时,就是完全背包问题,其余都不用变~)
2.完全背包 和 01背包 的区别仅在于状态更新时的遍历顺序。(即01是逆序,完全是顺序)
3.不超过容量 和 恰好装满 的区别仅在于二者的初始化~(前者全0;后者,f[i][0] = 0(第一列),其余全为0x80000000,一维:除f[0]为0外,其余f[j]都是负无穷)
4.循环边界 i:【1,N】,j:【V,weight[i]】(下文有讲,还有更优化的..不过看不懂)
5.多重背包问题可以拆分为 01背包 与 完全背包 的组合。
6.打印出装的顺序(下文有讲 尚未看)
原文说的很详细甚至到了啰嗦的地步..持续整理中...= =b
以下整理自:背包问题九讲笔记_01背包
摘自Tianyi Cui童鞋的《背包问题九讲》,稍作修改,方便理解。
01背包问题描述
已知:有一个容量为V的背包和N件物品,第i件物品的重量是weight[i],收益是value[i]。
限制:每种物品只有一件,可以选择放或者不放
问题:在不超过背包容量的情况下,最多能获得多少价值或收益
变型问题:在恰好装满背包的情况下,最多能获得多少价值或收益
这里,我们先讨论在不超过背包容量的情况下,最多能获得多少价值或收益。
基本思路分析
考虑我们的子问题,将前i件物品放到容量为v的背包中,若我们只考虑第i件物品时,它有两种选择,放或者不放。
1) 如果第i件物品不放入背包中,那么问题就转换为:将前i - 1件物品放到容量为v的背包中,带来的收益f[i - 1][v]
2) 如果第i件物品能放入背包中,那么问题就转换为:将前i - 1件物品放到容量为v - weight[i]的背包中,带来的收益f[i - 1][v - weight[i]] + value[i]
二维矩阵的实现代码(非最佳,因为可以使用一维矩阵,从而降低空间复杂度)
#include <iostream>
using namespace std;
const int N = 3;//物品个数
const int V = 5;//背包最大容量
int weight[N + 1] = {0,3,2,2};//物品重量
int value[N + 1] = {0,5,10,20};//物品价值
int f[N + 1][V + 1] = {
{0}};
/*
目标:在不超过背包容量的情况下,最多能获得多少价值
子问题状态:f[i][j]:表示前i件物品放入容量为j的背包得到的最大价值
状态转移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
初始化:f数组全设置为0
*/
int ZeroOnePack()
{
//初始化
memset(f,0,sizeof(f));
//递推
for (int i = 1;i <= N;i++) //枚举物品。i为0时无意义,故f[0][j] = 初始值0
{
for (int j = V;j >= 0;j--) //枚举背包容量
{
f[i][j] = f[i - 1][j]; //j<weight[i]时,状态仅能来自于<span style="font-family: Arial, Helvetica, sans-serif;">f[i - 1][j]</span>
if (j >= weight[i])
{
f[i][j] = max(f[i - 1][j],f[i - 1][j - weight[i]] + value[i]);
}
}
}
return f[N][V];
}
效率分析:
此算法的时间复杂度为O(N*V),空间复杂度也为O(N*V)。其中,N 表示物品个数,V 表示背包容量。
这里,时间复杂度不可以在优化了,但是空间复杂度可以继续优化到O(V)。
优化空间复杂度
上述的方法,我们使用二维数组 f[i][v] 保存中间状态,这里我们可以使用一维数组f[v]保存中间状态就能得到结果。
分析
f[i][j] = max(f[i - 1][j],f[i - 1][j - weight[i]] + value[i]),我们可以看出,状态的转移仅与上一行(i-1行)有关。
我们现在使用f[v]保存中间状态,我们想要达到的效果是,第i次循环后,f[v]中存储的是前i个物体放到容量v时的最大价值
当我们使用一维数组存储状态时,f[v]表示,在执行i次循环后(此时已经处理i个物品),前i个物体放到容量v时的最大价值,即之前的f[i][v]。与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[v]一直在重复使用,所以,也会出现第i次循环可能会覆盖第i - 1次循环的结果。
为了求f[v],我们需要知道,前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] 和 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + value[i]。
难点:
由于我们只使用一维数组存储,则在求这两个子问题时就没有直接取出那么方便了,因为,第i次循环可能会覆盖第i - 1次循环的结果。
现在我们来求这两个值:
1)前i-1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] :
由于,在执行在i次循环时,f[v]存储的是前i个物体放到容量v时的最大价值,在求前i个物体放到容量v时的最大价值(即之前的f[i][v])时,我们是正在执行第i次循环,f[v]的值还是在第i-1次循环时存下的值,在此时取出的 f[v]就是前i-1个物体放到容量v时的最大价值,即f[i - 1][v]。
2)前i-1件物品放到容量为v-weight[i]的背包中带来的收益,即之前的f[i-1][v-weight[i]] + value[i]
由于,在执行第i次循环前,f[0] ~ f[V]中保存的是第i - 1次循环的结果,即是前i - 1个物体分别放到容量0 ~ V时的最大价值,即f[i - 1][0] ~f[i - 1][V]。
则,在执行第i次循环前,f 数组中v - weight[i]的位置存储就是我们要找的 "前i - 1件物品放到容量为v - weight[i]的背包中带来的收益" (即之前的f[i - 1][v - weight[i]]),这里假设物品是从数组下标1开始存储(全文皆是)。
伪代码
for i=1..N //枚举物品
for v=V..0 //枚举容量,从大到小
f[v]=max{f[v],f[v-weight[i]] + value[i]};
由上面伪代码可知,在执行第 i 次循环时,