友友们好(^-^)🌹🌹🌹,我是杨枝,一枚在算法领域迈步的呆萌的博主呀~
目前还是一只纯纯的菜汪🐶。 典型的又菜又爱闹那种👀,做不好很多事,说不好很多话,写题还总不Ac😅,还在努力还在前进👣。
因为了,你们对我来说都是是独一无二的呀💓。在点开这篇文章的那一刻,我相信,我们之间相互需要彼此啦🌹🌹
时刻谨记:认真写算法,用心去分享。不负算法,不误卿。 感谢相遇(^㉨^)
🔔目录
💓简述动态规划
动态规划算法是把原问题视作若干个重叠子问题的逐层递进,每个子问题在求解的时候,姑且可以称为一个阶段。动态规划中,只有完成了前一个阶段的计算之后,才能执行下一个阶段的计算。 举个栗子
比如,我现在有一个数组叫做
d
p
[
j
]
dp[j]
dp[j],它的值只能够从
d
p
[
j
−
x
x
x
]
dp[j-xxx]
dp[j−xxx]推导计算出来。
为了保证这些计算能够顺序的、不重复的进行,动态规划已经求解的子问题不能受后续阶段的影响。那么动态规划对状态空间的的遍历可以视作一张有向无环图,这遍历顺序就可以当做对该有向无环图的拓扑排序。有向无环图中的节点对应问题中的“状态”,图中边则对应状态间的“转移”,转移的选取就是动态规划中的“决策”。
动态规划算法把相同的计算过程作用于各阶段的同类子问题,感觉上有点像把一个高中的数列一样了。
说简单粗暴一点,就可以理解为把一个固定的公式套用在格式相同地的若干输入数据上运行。这个公式被大多数人称为状态转移方程,我们做DP的主要心路历程也是用在分析出这个状态转移方程,最后由代码实现
💓01背包
🌟背景
题目可以抽象出来如下描述:
有一个体积为v的背包和一堆体积为
v
v
v,价值为
w
w
w的物品。每件物品最多只能用1次,所以咯,对于每个物品,要么用0次,要么用1次。故称为01背包问题
🌟例题描述
🎇🎇🎇原题传送门🎇🎇🎇[
🌟DP分析
照着y总讲授的闫式DP分析思路,照猫画虎的对01背包进行分析。
首先要清楚集合
f
[
i
,
j
]
f[i , j]
f[i,j]到底表示的是什么。
其次是要清楚要利用哪个属性进行求解。是最大值?还是数量?亦或是最小值.
对于背包问题了,大多数都是想要在有限的体积下获得最大的价值,所以一般是对“最大值”这个属性进行维护。
最后便是状态计算中对状态的划分,从而得到状态转移方程。
🌟状态转移方程优化
DP中,状态分析和优化是两个环节。一般来说是先进行状态分析获得朴素版本的状态转移方程。再看能不能再对空间和时间进行优化,是在得到朴素版本之后再做的考虑。
将上图的状态计算环节总结为下图
f
[
i
,
j
]
=
m
a
x
{
f
[
i
−
1
,
j
]
不选第i个物品
f
[
i
−
1
,
j
−
v
[
i
]
]
+
w
[
i
]
选第i个物品
f[i,j]=max \begin{cases} f[i-1,j]& \text{不选第i个物品}\\ f[i-1,j-v[i]] + w[i]& \text{选第i个物品} \end{cases}
f[i,j]=max{f[i−1,j]f[i−1,j−v[i]]+w[i]不选第i个物品选第i个物品
落实到代码上即如图
朴素版的DP的状态转移方程,我们发现,每一阶段 i i i的状态只会与上一个阶段的 i − 1 i-1 i−1有关。这种时候就可以使用滚动数组的方式进行优化,降低空间开销
落实到代码上即如图:
在上述程序中,我们把阶段i的状态存储在第一维的下标为i&1的二维数组中。当i为奇数的时候,i&1等于1;当i为偶数时,i&1等于0。因此,DP的状态就相当于在
f
[
0
]
[
x
x
x
]
f[0][xxx]
f[0][xxx]和
f
[
1
]
[
x
x
x
]
f[1][xxx]
f[1][xxx]两个数组中进行交替转移,空间复杂度从
O
(
M
N
)
O(MN)
O(MN)降低到
O
(
M
)
O(M)
O(M)
进一步分析,容易发现,在每个阶段开始时,实际上执行了一次从 f [ i − 1 ] [ x x x ] f[i-1][xxx] f[i−1][xxx] 到 f [ i ] [ x x x ] f[i][xxx] f[i][xxx]的拷贝操作。当我们省略掉 f f f数组的第一维。变成一维数组,即当外层循环到第 i i i个物品的时候,此时 f [ j ] f[j] f[j]表示背包中放入总体积为 j j j的物品最大价值之和
注意上述代码中的倒序循环:
f f f数组的后半部分 f [ j , m ] f[j ,m] f[j,m]处于“第i个阶段”,也就是状态计算时所做的图中的右半部分。
前半部分 f [ 0 , j − 1 ] f[0,j-1] f[0,j−1]处于“第i-1个阶段”,也就是状态计算时所做的图中的左半部分。
随着
j
j
j的不断减小,意味着我们总是用第
i
−
1
i-1
i−1个阶段的状态向第i个阶段的状态转移,和咱们最初推导的转移方程的想法是一致的
🌻参考代码(C++版本)
🎇🎇🎇点击这里查看参考代码喔🎇🎇🎇
💓完全背包
🌟例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌟DP分析法
完全背包依旧可以采用01背包中的分析方式,只是因为完全背包中,每件物品可以无限使用,因此在状态分析的时候会有点不同。
在这张分析图中,核心的核心是这个集合
f
[
i
,
j
]
f[i , j]
f[i,j]到底表示的是什么,这决定了后续的状态计算中的对集合的划分以及最后的答案输出等等
🌟状态转移方程优化
将上图的状态计算环节总结为下图
f
[
i
,
j
]
=
m
a
x
{
f
[
i
−
1
,
j
]
第i个物品选择1个
f
[
i
,
j
−
v
[
i
]
]
+
w
[
i
]
第i个物品选择1个
f[i,j]=max \begin{cases} f[i-1,j]& \text{第i个物品选择1个}\\ f[ i,j-v[i] ] + w[i]& \text{第i个物品选择1个} \end{cases}
f[i,j]=max{f[i−1,j]f[i,j−v[i]]+w[i]第i个物品选择1个第i个物品选择1个
初值
f
[
0
,
0
]
=
0
f[0 , 0] = 0
f[0,0]=0,其余的
f
[
0
,
1
f[0 , 1
f[0,1] 、
f
[
0
,
2
]
f[0 , 2]
f[0,2] 等等均为无意义
背包问题中是无法再优化时间的了,目前的优化只是优化空间。能否优化取决于是否具备单调性。
直接由状态转移方程编写的
d
p
dp
dp代码如下:
对于 f [ i , j ] f[i,j] f[i,j] = f [ i − 1 , j ] f[i -1 ,j] f[i−1,j]而言:
等式先计算右边的 f [ i − 1 , j ] f[i -1 ,j] f[i−1,j],再将计算结果赋值存放到 f [ i , j ] f[i,j] f[i,j]中。现在处于 i i i层循环,那么这个可以用来赋值的 f [ i − 1 , j ] f[i -1 ,j] f[i−1,j]肯定是从 i − 1 i-1 i−1而来的,故可以直接去掉数组的第一维。
对于
f
[
i
,
j
]
f[i,j]
f[i,j] =
m
a
x
max
max(
f
[
i
−
1
,
j
]
f[i-1,j]
f[i−1,j],
f
[
i
,
j
−
v
i
]
+
w
i
f[i ,j - vi]+wi
f[i,j−vi]+wi)而言:
f
[
i
−
1
,
j
]
f[i-1,j]
f[i−1,j]的处理方式和上面一致
j
j
j是从大到小遍历上来的,所有的
j
j
j >
j
−
v
i
j - vi
j−vi。因此,在需要用
j
−
v
i
j - vi
j−vi的值来就算
j
j
j的时候,他俩肯定都处于
i
i
i的同一层,而来数值更小的
j
−
v
i
j-vi
j−vi已经被算出来,故
f
[
i
,
j
−
v
i
]
+
w
i
f[i ,j - vi]+wi
f[i,j−vi]+wi中的
i
i
i可以直接去掉
优化后:
🌻参考代码(C++版本)
🎇🎇🎇点击这里查看参考代码呀🎇🎇🎇
💓多重背包
🌟例题描述——暴力解法版本
🎇🎇🎇原题传送门🎇🎇🎇
🌟DP分析法
多重背包问题和完全背包是很像的,完全背包问题中,物品是有无限件,但是多重背包中,对物品的数量进行了限制。
🌻参考代码(C++版本)
🎇🎇🎇点击这里查看参考代码喔🎇🎇🎇
🌟 例题描述——二进制优化
从道题的数据范围可以知道,假如直接暴力的话, 1000 ∗ 2000 ∗ 2000 1000*2000*2000 1000∗2000∗2000 = 40亿,C++ 一秒大概是算107 ~ 108次,所以暴力是一定会超时的。
🌟二进制优化的引入
倘若有个物品的个数
S
S
S,
S
=
1023
S = 1023
S=1023,想表示这个
S
S
S,首先可以想到的是从0开始枚举到1023。但是很明显这种效率很低。
假如使用打包的想法,每个包裹中物品的个数依次是
1
1
1、
2
2
2、
4
4
4、
8
8
8、
16
16
16、… … 、
512
512
512。那么,使用这十个包裹,就可以拼凑出0~1023中的任意数,比如7,就使用包裹中物品个数为1+包裹中物品个数为2+包裹中物品个数为4的三个包裹。
🌟二进制优化实例分析
倘若
S
S
S= 200了?,继续按照2的整次幂进行划分
1 ,2,4,8,16,32,64,
X
X
X。现在比较棘手的是最后一个包裹中物品的个数,它可以是128吗?假如是128,咱们从1累加到128,得到的是255,但是咱们只有200个物品,是凑不出来255的。
从1加到64是127,还差73。因此,当我们
X
X
X补成73的时候,1到64是可以凑出0~127中的任意数,0到127中的任意拼法加上73,就可以凑出73到200中任意数,整合起来,咱们确实可以凑出从0到200中任意数。
咱们需要注意的是对
X
X
X的限制,
X
X
X是必须严格小于2k+1
具体的流程落实
🌻参考代码(C++版本)
🎇🎇🎇点击这里查看参考代码🎇🎇🎇
💓分组背包
🌟例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌟DP分析法
分组背包最主要的特点是关注选哪个组的第几个物品。
因为只能选择一个,就可以用分析01背包的方式进行分析了,只是在选择某组的第非0个物品时候,要具体落实到第几组的第几个物品,比如第 i i i组的第 k k k个物品
🌻参考代码(C++版本)
🎇🎇🎇点击这里查看参考代码🎇🎇🎇
💓总结
背包问题在动态规划也算是独霸一席天下了,本文系统的介绍了常见的四种背包问的处理方式。最主要的了,在动手之前,一定要捋清楚这个集合 f [ i , j f[i,j f[i,j]到底是要表示什么。其次再考虑状态计算呀、优化呀。其余的背包问题,笔者后续使用题解的方式逐一分享