Dynamical Programming(动态规划)
这是一种对递归方法的优化,应用地最多的场合是组合优化问题(Combinatorial Optimization)。
在用一般递归方法解决某些问题的时候,会出现对同一个子问题的重复计算,例如:
f(n) = f(n-1) + f(n-2)
f(10) = f(9) + f(8) = (f(8) + f(7)) + f(8) = ...
例如上面的f(8)就被计算了很多次。 所以,动态规划实现了,在解决子问题的过程中,保存这些子问题的答案,供后面直接使用,避免重复计算。
动态规划的例子包括:
1、最长公共子序列问题(L[i,j])
2、矩阵链相乘的最小代价问题(C[i, j])
3、图的最短路径问题(Floyd算法)
4、背包问题 (V[i, j])
以上例子基本都是通过二维矩阵来保存每次递归计算的中间结果的。
而且这个中间结果也是子问题的最优解。
----
动态规划要点
1. 问题怎么表示:整理入参,形成一个向量。例如,只有一个入参,那么问题可以表示为 answer[k] (为什么是数组,因为把子问题也表示了)。
有的人也把子问题叫做“状态”。
2. 结果怎么表示:例如,表示成一个向量。问题的有些部分看似是变量,其实每次运行的时候可以视为固定的、是常量。
3. 问题怎么分解:针对入参进行分解,让入参变小。。分解之后,往往可以写出一个递推公式(状态转换)。
先弄清楚问题是几维的,一维f(n), 二维f(i, j)
不同问题之间的区别主要体现在这里,即如何分解。
分解的时候可以逆向思考,即假如已经得到最终结果n,他跟上一步结果n-1之间是什么关系。
进行问题分解的时候,往往可以在一个问题的末尾(例如某个序列的最后一个元素的地方)执行分类讨论。
分解问题的时候可能产生一个新问题,不过这个新问题的限制条件更多,更容易解决。
4. 根据这个递推公式,采用自底向上的方法,写代码进行实现。一般是一个循环,从小到大计算出最终的问题。
最长递增子序列问题的解
L(i) = max(L(i-1), S(i));
S(i) = for k=0 to i-1: (a[k] < a[i] ? S(i - k) + 1 : 1);
说明:L(i)是最长递增子序列长度, S(i)是必须包含元素i的,a[1..i]范围内的最长递增子序列(比L(i)多一个条件,必须包含a[i]这个数组元素)
int lis(int[] nums) {
int n = nums.length;
int[] L = new int[n];
int[] S = new int[n];
L[0] = S[0] = 1;
for ( int i = 1; i < n; ++i ) {
S[i] = 1;
for ( int j = 0; j < i; ++j ) {
if ( nums[j] < nums[i] ) {
S[i] = max(S[j] + 1, S[i]);
}
}
L[i] = max(L[i - 1], S[i]);
}
return L[i];
}