如需转载,请注明出处。 个人代码专栏见:https://github.com/alphaplato/Cplusplus/tree/master/DynamicProgram 动态规划问题是计算机编程中经常遇到的一类问题,尽管作为标准的算法工程师,比如语音识别、nlp等领域的工程师,并不经常遇到动态规划的问题,但是作为计算机工程师,或者说编程从业者却是一类需要重点关注的问题。至少有一点,互联网领域内的算法工程师与开发工程师在求职过程中,将会经常与这类问题打交道。 这个项目将会就动态规划问题进行集中的论述。本项目将以典型的动态规划问题依次展开,一方面给出相应问题的解决思路,另一方面给出对应的C/C++代码,同时笔者也会在未来不断丰富该问题相关知识。欢迎您能致函(plato.sg.lee@gmail.com),给予纠正、建议或其他诉求。 ## 目录 1、费布那契数列 2、背包问题 3、最长公共子序列 4、最长公共子串 5、最长递增子序列 6、最小编辑距离(莱文斯坦距离) ## 内容 ### 1、费布那契数列 >斐波那契数列(意大利语:Successione di Fibonacci),又译为菲波拿契数列、菲波那西数列、斐波那契数列、黄金分割数列。 >在数学上,费波那契数列是以递归的方法来定义: > * $F_0=0$ > * $F_1=1$ > * ... > * $F_n=F_{n-1}+F_{n-2}$ > * $F_{n}=F_{n-1}+F_{n-2}(n≧2)$ >用文字来说,就是费波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出。首几个费波那契系数是: >0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……(OEIS中的数列A000045) >特别指出:0不是第一项,而是第零项。 根据以上的定义可知,费布那契数列在的标准实现是递归,也就是当我们求Fibonacci的第n个数Fibonacci(n)时,通常情况下用简单的递归即可实现。 但是今天我们用动态规划实现。为什么要用递归实现费布那契数列呢?因为费布那契的动态规划解决方案实际上是最直观的动态规划问题方案,最能浅显地表达动态规划的思想。 解决方案: * 目标:求解第n的Fibonacci数列的值; * 定义dp[i]:表示第i个数Fibonacci数列的值; * 转移方程:dp[i]=dp[i-1]+dp[i-2], i>=2; * 边界条件:dp[0]=0,dp[1]=1。 看,通过数组把每一个状态值记录下来,而在一个状态值从前面的状态值产生,我们要做的就是在给定了n的时候,从0、1、2开始计算,直至计算出n-2、n-1,又计算出n即最终n的结果。 如果看懂了费布那契,便看懂了世界,动态规划的世界。个人理解上动态规划的本质就是在设计依赖,逐步得出最终的结果,像动态规划求解费布那契(第n值)一样。 代码实现见[fibonacci.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/fibonacci.cc)。 ### 2、背包问题 #### 01背包 >我们有n种物品,物品j的重量为w[j],价值为v[j]。 >我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W。 >如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题。 >可以用公式表示为: >最大化 $sum_{j=1}^{n}v_{j}x_{j}$ >受限于 $sum_{j=1}^{n}w_{j}x_{j}\quad x_{j} \in {0,1}$ 由以上定义可知01背包问题可简述为有一组物品具备不同的重量和价值且唯一,在给定背包载重条件下,求背包可容纳最大的价值。 解决方案: * 目标:求给定背包载重,求其可容纳最大价值; * 定义dp[i][j]:前i个物品中在背包载重j时容纳的最大价值; * 转移方程:dp[i][j]=max{ * dp[i-1][j] 当j<w[j], * dp[i-1][j-w[j]]+v[j] 当j>=w[j]且选择j物品情况, * dp[i-1][j] 当j>=w[j]且不选j物品情况}; * 边界条件: * dp[i][0]=0,当载重为零时; * dp[0][j]=0,当j<w[0],即背包装不进第一个物品时。 ps:一道推荐算法面试题 >在短视频推荐时,已知一个用户观看视频的总时长和该用户候选视频的ctr(点击率),候选视频的播放时常已知,假定用户一旦点击就看完,求用户可观看视频的最大数量。 #### 无界背包 >如果不限定每种物品的数量,则问题称为无界背包问题。 解决方案: * 目标:求给定背包载重,求其可容纳最大价值; * 定义dp[i][j]:在背包载重j时容纳的最大价值; * 转移方程:dp[i]=max{ * dp[i-w[j]] +v[j] 当i>=w[j]时(0=<j<=n), * 0 当i<w[j]}; * 边界条件:dp[0]=0。 代码实现见[knapsack.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/knapsack.cc)。 ### 3、最长公共子序列(长度)(Longest Common Subsequence,LCS) > 最长公共子序列(LCS)是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题。这与查找最长公共子串的问题不同的地方是:子序列不需要在原序列中占用连续的位置 。 > 如:"BDCABA" 和 "ABCBDAB"的一个最长公共子序列 "BCBA"; > 可以不连续、不唯一。 解决方案: * 目标:求给定两个字符串A、B后,它们的最长公共子序列长度; * 定义dp[i][j]:字符串A前i个字符的子串和字符串B前j个字符的子串的最长公共子序列长度; * 转移方程:dp[i][j] = max{ * d[i-1][j-1] + 1 当A[i-1]=B[j-1]时, * d[i-1][j] 当A[i-1]!=B[j-1]时, * d[i][j-1] 当A[i-1]!=B[j-1]时}; * 边界条件: * dp[0][j] = 0; * dp[i][0] = 0。 注意dp[i][j]的定义使得在具体实现时最一个对应状态的最后一个字符分别为A[i-1]、B[j-1]。 代码实现见[lcs.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/lcs.cc)。 ### 4、最长公共子串(Longest Common Substring,LCSS) > 在计算机科学中,最长公共子串问题是寻找两个或多个已知字符串最长的子串。此问题与最长公共子序列问题的区别在于子序列不必是连续的,而子串却必须是。 > 字符串"ABABC","BABCA"以及"ABCBA"的最长公共子串是"ABC"。其他的公共子串包括> "A"、"AB"、"B"、"BA"、"BC"以及"C"。 解决方案: * 目标:求给定两个字符串A、B后,它们的最长公共子序列长度; * 定义:定义dp[i][j]:字符串A前i个字符的子串和字符串B前j个字符的子串在包含第i和j个字符时的最长公共子串长度; * 转移方程:dp[i][j] = max{ * d[i-1][j-1] + 1 当A[i-1]=B[j-1]时, * 0 当A[i-1]!=B[j-1]时}; * 边界条件: * dp[i][0] = 0; * dp[0][j] = 0。 从定义可知这里面的dp[i][j]并不代表字符串A前i个字符的子串和字符串B前j个字符的子串的最大公共子串,因此最终结果不是dp[n][m] (假设n,m分别为A,B长度),而是dp[i][j]的最大值。 代码实现见[lcss.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/lcss.cc)。 ### 5、最长递增子序列(LIS) > 在计算机科学中,最长递增子序列(longest increasing subsequence)问题是指,在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。许多与数学、算法、随机矩阵理论、表示论相关的研究都会涉及最长递增子序列。解决最长递增子序列问题的算法最低要求O(n log n)的时间复杂度,这里n表示输入序列的规模。 > 1. 对于以下的原始序列 > 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 > 最长递增子序列为 > 0, 2, 6, 9, 11, 15. > 2. 值得注意的是原始序列的最长递增子序列并不一定唯一,对于该原始序列,实际上还有以下两个最长递增子序列 > 0, 4, 6, 9, 11, 15 或 0, 4, 6, 9, 13, 15 解决方案: * 目标:给定字符串A,求出其最长递增子序列; * 定义dp[i]:表示包含A[i]字符时,A[0:i]的最长递增子序列; * 转移方程dp[i] = max{ * d[j]+1 当A[j] > A[i]且j<i时, * 1 当A[j]<=A[i]且j<i时}; * 边界条件dp[0] = 1。 从定义可知这里面的dp[i]并不代表字符串A前i个字符的子串和字符串B前j个字符的子串的最大递增子序列,因此最终结果不是dp[n] (假设n分别为A),而是dp[i]的最大值。 代码实现见[lis.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/lis.cc)。 ### 6、最小编辑距离 > 最小编辑距离,又称莱文斯坦距离(Levenshtein)距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。 > 例如将kitten一字转成sitting: > * sitten (k→s) > * sittin (e→i) > * sitting (→g) > 俄罗斯科学家弗拉基米尔·莱文斯坦在1965年提出这个概念。 解决方案: * 目标:求给定两个字符串A、B后,计算它们的最小编辑距离; * 定义dp[i][j]:字符串A的前i个字符的子串到为字符串B前j个字符的子串的最小编辑距离;注意前i意味着字符串最后一个字符为i-1; * 转移方程:dp[i][j] = min{ * dp[i-1][j-1] 当A[i-1]=B[j-1], * dp[i][j-1] + 1 当A[i-1]!=B[j-1], //插入操作 * dp[i-1][j] + 1 当A[i-1]!=B[j-1], //删除操作 * dp[i-1][j-1] + 1 当A[i-1]!=B[j-1] //替换操作}; * 边界条件: * dp[0][j] = j; * dp[i][0] = i。 代码实现见[levenshtein.cc](https://github.com/alphaplato/Cplusplus/blob/master/DynamicProgram/levenshtein.cc)。
动态规划经典问题
最新推荐文章于 2024-07-13 16:30:23 发布