前言
动态规划在《数据结构与算法》中接触过,随后在《软件设计师教程》中也学习过,每次学完没有做记录,感觉会了,但实际上还是有些不理解的。这次在《图解算法》中重新接触到动态规划,讲得挺透彻的,应该要好好记录下来。无论动态规划的问题有多么形式多样,只要了解了解题思路和原理。再多加练习,肯定能掌握的。
基本思想
其基本思想是将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
这是《软件设计师教程》中对动态规划思想的描述。看起来很简单,就把大问题划成小问题。恰恰难的也就是如何将大问题化解成小问题。通过这篇文章,来慢慢体会这个划分的过程。
经典背包问题
背包可以装 4 磅;
音响 3000元,4磅;
笔记本电脑,2000元,3磅;
吉他,1500元,1磅。
书中是直接提供的自底向上解题的过程。
在这里我们先从上至下分析一下问题:
拿还是不拿?
对于每一个物品,我们都有两个选择,拿或者不拿。我们需要解决的是:拿哪些能达到最高价值?
现在我们什么都没拿,0价值,4磅空间。我们先考虑吉他:
拿吉他:现在背包价值1500,剩余空间3磅。
不拿吉他:背包价值0,剩余空间4磅。
然后在拿或不拿的基础上考虑音响,
拿了吉他,价值1500元,剩余3磅空间:
拿音响:音响占用空间4磅 > 剩余空间3磅,所以拿不下。现在背包价值1500,剩余3磅空间。
不拿音响:和原来一样,背包价值1500,剩余空间3磅。
不拿吉他,价值0元,剩余4磅空间:
拿音响:背包价值3000,剩余0空间。
不拿音响:背包价值0,剩余4空间。
然后在前两者组合的基础上再考虑笔记本电脑,
其中组合为:
1.拿了吉他,拿不下音响:剩余3空间
2.拿了吉他,不拿音响:剩余3空间
3.没拿吉他,拿音响:剩余0空间
4.没拿吉他,也没拿音响:剩余4空间
我们发现,1和2剩余的3磅空间刚好再拿个笔记本电脑,且1组合价值最高,而4组合仅拿了电脑,价值为2000,不如1组合。
似乎到这里我们仍然得不到什么结论,以上的方法,类似一个排列组合,查看所有的组合方式中的最高价值。但是我们却已经完成了 背包问题的第一步:刻画最优子结构
拿了某物品,和剩下空间的最优解
不拿某物品,和剩下空间的最优解
一点一点挤
我们完成了从上到下的分析,子问题是在剩下的空间里选择最值钱的物品。然后考虑第二步:合并子问题,也就是自下而上合并。
假如剩下1空间,我们拿什么最划算?
假如剩下2空间,我们拿什么最划算?
……
因此我们需要画一个表格用来记录子问题的最优解:
行:当前行和之前行表示可以选择的物品
列:表示背包的剩余空间
因为我们考虑的是剩余空间,因此填充列:
剩余空间1:很明显,剩余空间1时,我们只能拿吉他,因此在第一列,我们填入吉他与价格:
1 2 3 4 吉他(G) 1500 (G) 音响(S) 1500 (G) 笔记本 (C) 1500 (G)
剩余空间2:我们仍然不能拿其他的物品,因此在第二列,我们填入吉他与价格:
1 2 3 4 吉他(G) 1500 (G) 1500 (G) 音响(S) 1500 (G) 1500 (G) 笔记本 (C) 1500 (G) 1500 (G)
剩余空间3:在这里,我们发现,当遇到笔记本电脑时,可以装下而且比吉他价格高,因此我们在笔记本行填入笔记本:
1 2 3 4 吉他(G) 1500 (G) 1500 (G) 1500 (G) 音响(S) 1500 (G) 1500 (G) 1500 (G) 笔记本 (C) 1500 (G) 1500 (G) 2000 (C)
剩余空间4:在这里我们又发现,遇到音响时,可以装下,且价格为3000:
1 2 3 4 吉他(G) 1500 (G) 1500 (G) 1500 (G) 1500 (G) 音响(S) 1500 (G) 1500 (G) 1500 (G) 3000 (S) 笔记本 (C) 1500 (G) 1500 (G) 2000 (C)
最后一格怎么办呢,现在最大价格是3000,如果拿电脑,就装不下音响,价值只有2000,价值没有之前高。但 如果拿电脑,背包则还可以装1空间的物品。因此我们看看空间剩1时,可以装的最高价格是什么东西。我们之前表中已经填过,是吉他。且笔记本+吉他的价格为3500,超过了音响的价格。所以我们在最后一格中填入
1 2 3 4 吉他(G) 1500 (G) 1500 (G) 1500 (G) 1500 (G) 音响(S) 1500 (G) 1500 (G) 1500 (G) 3000 (S) 笔记本 (C) 1500 (G) 1500 (G) 2000 (C) 3500(C/G)
在这里我们发现,如果可选择的物品重量大于剩余空间时,是不能放入背包的。而当前可选物品可以放进去时,就需要进行比较。那么,是比较什么呢?至此,我们可以得到计算剩余空间最大价值的公式 :
因为当前行是需要选择是否拿的物品,所以使用上一行的值作为参照。
比如第三行3列:笔记本,剩余空间3时,就要选择比较:
拿:放下吉他,价值2000,剩余空间0
不拿:价值1500,剩余空间3
显而易见,拿比不拿价格高。
音响同理;
在比如第三行3列:笔记本,剩余空间4时,选择比较如下:
拿:放下音响,价值2000,剩余空间1。还可以拿吉他,一共价值3500
不拿:价值3000,剩余空间0
显而易见,拿比不拿价格高。
代码实现
1 | #include <stdio.h> |
运行结果如下:
最长公共子序列
《软件设计师教程》中的描述:
子序列是指从给定序列中随意地(不一定是连续的)去掉若干元素后所形成的序列。如X = ABCBDAB , Y = BCDB,则Y是X的一个子序列。若给定两个X和Y,如果Z同时是X和Y的子序列,那称Z是X和Y的公共子序列。最长公共子序列问题定义为:给定序列X和Y,求这两个序列的最长公共子序列。
有点抽象化,所以看起来很费劲,那看一下《图解算法》的例子:
那我们怎么猜用户应该输入的是什么呢?
是fish?
还是vista?
当然是看和哪个单词重合度更高一点,因此我们便采用最长公共序列算法。
谁和谁一样
使用fish和hish不太能体现最长序列的分析,我们采用软考教程的例子:
X = ABCBDAB
Y = BDCABA
人工判断的话,自然是先在两个序列中找到第一个相等的,记下来
AB & B -> B
然后再往后找第二个一样的
C & DC -> C
也就是 ABC & BDC - > BC
继续
B & AB -> B
也就是 ABCB & BDCAB -> BCB
然后
DA & A -> A
也就是 ABCBDA & BDCABA -> BCBA
Y序列已经没有字符了,所以我们也就找完了。可以发现,后面被寻找的内容都是在前面找到的相等的字母后面,也就是说CBDA & DCABA - > CBA 。都去除一个相同的字母结尾的序列,剩下的公共子串也是其剩余序列的最长公共子串。
一个一个比
那计算机应该怎么进行比较呢,它不能像人一样一次比较多个字母,也不知道何时两个字母会相等。所以就要一个一个字母进行比较,同时记录比较情况。
X = FISH
Y = HISH
F | I | S | H | |
---|---|---|---|---|
H | ||||
I | ||||
S | ||||
H |
这个表记录的就是子字符串的公共子串字母个数。
按行进行分析
第一行,H 分别与 F 、 FI 、 FIS 、 FISH 的最大子串
第二行,HI 分别与 F 、 FI 、 FIS 、 FISH 的最大子串
第三行,HIS 分别与 F 、 FI 、 FIS 、 FISH 的最大子串
第四行,HISH 分别与 F 、 FI 、 FIS 、 FISH 的最大子串
按照之前分析的,如果字母相等的话,就去掉相同字母的最长公共子串 + 1 。在不相等的情况下,则需要判断去掉比较双方的其中一个之后哪一个的 值比较大,以记录最长的公共子串。
如
HI & FI : I 相等,那就 H & F(去掉比较字符I )的最长公共子串值 0 + 1 。
HI & FIS : I 不等于 S , 选择 HI & FI (去掉 S) 的值 1 与 H & FIS (去掉 I)的值 0 中较大的一个。
依次类推得到下表:
F | I | S | H | |
---|---|---|---|---|
H | 0 | 0 | 0 | 1 |
I | 0 | 1 | 1 | 1 |
S | 0 | 1 | 2 | 2 |
H | 0 | 1 | 2 | 3 |
这里我们知道了最终结果的值是3,但是好像看不出来公共子串的序列是什么。所以我们需要记录序列路径。
上表示 去掉比较的行字符 比 去掉比较的列字符 更大
左表示 去掉比较的列字符 比 去掉比较的行字符 更大
左上表示 比较字符相等,去掉比较字符之后 + 1
x | F | I | S | H | |
---|---|---|---|---|---|
y | 0 | 0 | 0 | 0 | 0 |
H | 0 | 0 ↑ | 0 ↑ | 0 ↑ | 1 ↖ |
I | 0 | 0 ↑ | 1 ↖ | 1 ← | 1 ↑ |
S | 0 | 0 ↑ | 1 ↑ | 2 ↖ | 2 ← |
H | 0 | 0 ↑ | 1 ↑ | 2 ↑ | 3 ↖ |
这时我们便可以看到,从 H -> S -> I 逆序过来就是最长公共子序列的字母了。
代码实现
1 | #include <stdio.h> |
结果如下
总结
- 动态规划解决的是问题具有最优子结构,总问题的解可以由子问题的解合并得来
- 子问题具有重叠性,有许多的子问题是重复的
- 采用填表法,即记录子问题的解不再重复结算