相比于链表,字符串,或者暴力的直接回溯问题,动态规划问题是有难度的。它的难点在于不像前面的问题那么直接,
需要我们自己去寻找规律。这个规律也就是问题如何拆分的规律。往往我们为了求解当前的状态,找到它跟前面的状态的联系,借助于前面的状态,快速地求解得到当前的状态。
下面我将从简单的动态规划实例,动态规划与回溯的区别,典型的动态规划这几个方面来讲解。
1. 简单的动态规划实例
我们来看一下斐波那契问题。比如我们需要计算前 n 个斐波那契数。
我们当计算到第 i 个 值的时候,我们知道F ( i ) = F ( i - 1 ) + F ( i - 2 )。
如果我们之前已经存储下来 F ( i - 1 ) 和 F ( i - 2 ) 的值,我们是不是就能够很轻松地计算出F ( i )了?
要是没有存储的,我们需要将 F ( i - 1)继续展开成 F ( i - 2 ) + F ( i - 3 ),直到到达 F ( 0 )或者 F ( 1 )。
(如果不存储,计算 F ( n ) 的复杂度将会是将会随 n 指数增长 ,存储的话,计算复杂度为O ( n ), 差别是多么大啊! )
在这里, F ( i ) 中间状态。F ( i ) = F ( i - 1 ) + F ( i - 2 ) 为我们的转移矩阵。
很多时候动态规划的难点在于如何定义中间状态和寻找转移矩阵。
2. 动态规划与回溯的区别
动态规划跟回溯很像,就是将大问题问题拆分成小问题的思想。
一般而言,如果要找出所有符合的结果,这时候就应该回溯。
如果是需要找出最大的,最小的这类,就该用动态规划了。
3. 典型的动态规划
比如换零钱问题, 给出各种面值的零钱array,和一个总的钱数目 n ,找到最小零钱个数的兑换。
在这里,中间状态可以表示总的钱的数目为 i 的最小兑换,
状态转移矩阵为 F ( i ) = min ( F ( i - array [ 0 ] ) , F ( i - array [ 1 ] ) , ... ,F ( i - array [ array.lenght - ] ) ) + 1;
又比如找到乘积最大的拆分,将正整数 n 进行拆分成更小的正整数,使得这些拆分的数的乘积最大。
在这里,中间状态为整数 i 进行拆分,得到的最大面积,
状态转移矩阵为 F ( i ) = max ( 1 * ( max ( i - 1, F ( i - 1 ) , ... , F ( i - j ) ,2 * ( max ( i - j, F ( i - j ) + ...
+ F ( i - i / 2 ) ,i / 2* ( max ( i - i / 2, F ( i - i / 2 ) ;
中间的max是可以选择拆分也可以选择不拆分。
比如Longest Increaing Subsequence, 要找数组中的最长递增子序列。
在这里,中间状态可以表示下标为 i 的时候,能达到的最大递增序列,
状态转移矩阵为 F ( i ) = max ( 1,cache [ j ] + 1 ) 其中 j < i, 且满足 A [ j ] < A[ i ]。
其他的动态规划问题还有:
2. Unique Paths 寻找从起点到终点的路径个数3. Unique Paths II 带有障碍的网格中起点到终点的个数
7. Best Time to Buy and Sell Stock 买卖股票的最好时间
9. Maximum Product Subarray 乘积最大的子数组
10. Range Sum Query - Immutable 一段范围内的值查询
11. Range Sum Query 2D - Immutable 计算矩形中的元素和
12. Counting Bits 计算二进制形式中的1的个数
14. Perfect Squares 平方数的组合Integer Break 找到乘积最大的拆分
17. Longest Valid Parentheses 最长的合法括号序列
18. Distinct Subsequences 不同的子序列个数
总结:
解动态规划的问题的关键在于找出当前的状态怎么借助之前的状态得到。
为了更好地掌握,需要思考,并且练习,并且总结。