前言
从今天开始陆续给大家讲一下动态规划中十分重要的一个问题——背包问题,主要有三种具体的类型,0-1背包、完全背包、多重背包。这三种背包问题是使用动态规划思想的十分重要且典型的问题,在面试和笔试中出现的频率很高,强烈建议大家掌握。今天这篇博客先进行预热,给大家讲一下0-1背包。在此之前先放一个链接:【动态规划】01背包问题,这篇博客是我到现在为止看到的讲0-1背包问题最好的博客,我也是看了这篇博客后渐渐理解了背包问题,因此建议大家看一下这篇博客,当然了,也希望大家看完之后也可以赏光一下本人的这篇博客,顺便麻烦大家提点改进意见。好,下面进入正题。
概念准备
因为背包问题属于动态规划的思想,所以有必要给大家稍微介绍一下动态规划。首先动态规划主要是用来找最优选择的,也就是说解决问题的方法有很多,但我需要从中找出最优的解法,举个最简答的栗子:你去超市买东西,收银员找你零钱的时候,他其实不只有一种找零钱的方案,比如他需要找你10块,那他可以给你5个一块和1个五块的,也可以给你10个一块的,当然也可以给你2个五块的或直接给你1个十块的,一般情况下这几种找法没什么区别,但商家可能希望尽量剩下些零钱,也就是说可以的话,优先给你找1个十块的或2个五块的,实在不行再给1块的,这就是典型的求最优解的问题,在leetcode中有一道这类的题大家可以区刷一下,它使用的就是动态规划的思想而且是动态规划的完全背包的问题。
但并不是所有的找最优解的问题都可以使用动态规划,一般来说它需要满足两个特点,首先是最优子问题,也就是说我这个需要解决的最优问题是一个全局最优解,它是由若干个最优子问题组成的,这时我们将这个全局问题分解为若干子问题,找到这若干子问题的解那全局最优解也出来了。再举个栗子,比如我现在从一个城市A开车到另一个城市B,然后有很多条路可以选择,那我肯定要选距离最短的路对吧,因为这样省油,现在我知道所有可能经过的城市和各个相邻城市之间的距离,那我怎么算出最短距离呢?假如我们查看地图发现有一个城市C是必经之路,那这时我们可以将求A到B之间的最短距离变成求A到C和C到B之间最短距离的和,当然在实际问题中这个“必经之路”可能不易观察,那我们可以使用枚举的方式一个个去比较,然后我们求这两个子问题时也可以使用这种方法,知道我们找到一个直接可得的最优子问题。
除此之外这其中还暗含着另外一个动态规划思想的条件,那就是无后效性,我把他理解为独立性,从A到C和C到B之间的最短距离是互不影响的,也就是说我求A到C之间的最短距离不必考虑C到B之间的最短距离。这样将两者的最短距离加一起才是A到B的最短距离。当所求问题满足这两个条件时就适合使用动态规划的思想,这是我个人的理解,如果听不太明白的可以参考上面推荐的博客或其他资料,好,理论基础到此为止,我们开始实战。
0-1背包
场景
我记得很早之前看过这样一档节目,节目中有一个环节,给嘉宾发一个购物车,然后准备一些商品,每个商品只有一个(这是重点一定要注意,不然就不是0-1背包问题),在1分钟的时间内嘉宾可以随意往购物车中装任意商品,只要购物车装的下,然后购物车里面的商品就属于嘉宾了,那对于嘉宾而言他有很多种选法但哪一种选法购物车中的商品的价值最大呢?这就是一个典型的0-1背包问题,为了便于讲解,我们建立一下数学描述模型。
数学模型
我们使用变量V描述购物车的容量;
使用P描述所选商品的总价值;
然后使用两个含有n个元素的集合:
vi
∈
\in
∈{v1,v2,v3,…,vn},(0<=i<=n),vi表示第i种商品的体积;
pi
∈
\in
∈{p1,p2,p3,…,pn},(0<=i<=n),pi表示第i种商品的价值。
最后我们再用一个变量xi表示第i种商品的个数,因为每种商品只有1个,所以它只要两个值0或1。我们的目的是求所有商品的最大值,即:
P
=
m
a
x
(
p
1
x
1
+
p
2
x
2
+
.
.
.
+
p
i
x
i
)
P=max(p_1x_1+p_2x_2+...+p_ix_i)
P=max(p1x1+p2x2+...+pixi)
这个结果需要符合一个约束:
V
>
=
(
v
1
x
1
+
v
2
x
2
+
.
.
.
+
v
i
x
i
)
V>=(v_1x_1+v_2x_2+...+v_ix_i)
V>=(v1x1+v2x2+...+vixi)
这个最优解我们一上来可能不知道该如何求,但我们仔细思考一下它其实符合动态规划的思想,我们定义函数P=func(i,j)表示体积j下前i种商品组合的最优解,当i=n,j=V时结果即所求,那子问题该怎么定义呢?或者说这个全局最优解依赖于哪一个局部最优解呢?我们这样想,一共i个商品,我们先不考虑多个商品,我们先只考虑第i个商品,这个商品要么选要么不选,所以这个最优解只可能是这两种情况中的一种:
如果选,则
P
=
f
u
n
c
(
i
−
1
,
j
−
v
i
)
+
p
i
P=func(i-1,j-v_i)+p_i
P=func(i−1,j−vi)+pi
如果不选,则
P
=
f
u
n
c
(
i
−
1
,
j
)
P=func(i-1,j)
P=func(i−1,j)
两者取最大,所以
P
=
m
a
x
{
f
u
n
c
(
i
−
1
,
j
)
,
f
u
n
c
(
i
−
1
,
j
−
v
i
)
+
p
i
}
P=max \{ func(i-1,j),func(i-1,j-v_i)+p_i \}
P=max{func(i−1,j),func(i−1,j−vi)+pi}
然后对于这两个子问题我们同样可以采用这种方法去求,这样思想就是所谓自上而下,我们经常使用递归的方式去实现,递归终止条件是func(i,j)=0(i=0 || j=0)。大家可以尝试去编写一下,相信到这个地步对大家而言不会太难。因为递归的调用时间过长,我们着重讲一下自下而上的方法,为了更好理解,我们采用填表的方式进行讲解。
自下而上填表法
首先我们具体化一下例子以便进行填表,pi ∈ \in ∈ {2,4,3,7} ,vi ∈ \in ∈ {2,3,5,5},这样n=4,我们假如购物车容量V=10,求最大价值P。我们构建这样一张表:第一行表示j,第一列表示i,每一格表示此时的最大价值P,所求最大值P=p(4,10)。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | |||||||||||
1 | |||||||||||
2 | |||||||||||
3 | |||||||||||
4 |
首先j=0或i=0时P=0。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||||||||
2 | 0 | ||||||||||
3 | 0 | ||||||||||
4 | 0 |
然后填i=1行,即只有一个商品时,只有两个选择选或不选。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | ||||||||||
3 | 0 | ||||||||||
4 | 0 |
然后填i=2,如当j=4时,p(2,4)=max{p(1,1)+4,p(1,4)}=4。之后同理。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 2 | 4 | 4 | 6 | 6 | 6 | 6 | 6 | 6 |
3 | 0 | 0 | 2 | 4 | 4 | 6 | 6 | 6 | 7 | 7 | 9 |
4 | 0 | 0 | 2 | 4 | 4 | 7 | 7 | 9 | 11 | 11 | 13 |
最后求得p(4,10)=13。
可以看出,除i=0或j=0时p(i,j)=0,每一行都依赖于上一行的解,并且每一行中每个解依赖于之前的解。所以特别适合自下而上的动态规划。下面我们进行代码实现。
代码实现
class solution {
vector<int> _p; //商品价值集合
vector<int> _v; //商品体积集合
int n; //商品数量
int max_v; //购物车容量
public:
solution(const vector<int>& p, const vector<int>& v, int m) {
_p = p;
_v = v;
n = _p.size() - 1;
max_v = m;
}
int func() {
vector<vector<int>> dp(n+1, vector<int>(max_v+1, 0));
for (int i = 0; i <= n; i++)
dp[i][0];
for (int j = 0; j <= max_v; j++)
dp[0][j];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= max_v; j++) {
if (j < _v[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - _v[i]] + _p[i]);
}
}
return dp[n][max_v];
}
};
这里我把这个函数和所需数据封装成一个类,但也可以只封装接口,看各位的习惯了。也建议大家自己先写一下代码,有了上述的算法基础这个代码其实不难,好了01背包到此为此,后续将再更一篇博客讲完全背包和多重背包。