漫画:什么是动态规划? (写得很不错,推荐看)
动态规划 dynamic programming 通俗称为dp
核心思想1:大事化小,小事化了。
核心思想2:记住原来计算过的问题。(也就是重叠子问题,加快计算速度)
最快理解动态规划的例子:
斐波那契数列 0,1,1,2,3,5,8....
规律就是F(n)=F(n-1)+F(n-2)
最初始的边界为F(0)=0,F(1)=1;
我们第一时间能想到的应该是这个计算方法:
public static int fibSum(int n) {
if (n == 0 || n == 1) {
return n;
} else {
return fibSum(n - 1) + fibSum(n - 2);
}
}图片来自上面小灰的链接,侵权立删
然后我们来看这中计算方法的时间复杂度,这个计算过程就是一颗二叉树,它的深度为n-1,由二叉树的性质可知它有接近2的n-1次幂个节点。而每个节点就是调用一次这个方法,所以时间复杂度可判定为2的n次方。
然后我们看到这个计算过程中有大量重复计算过程,左子树的子树里有右子树,所以就进行了大量的重复计算。所以这个时候我们就知道利用key的唯一性使用hashmap进行存储,
private static final HashMap hm = new HashMap();
private static int fibSum(int n) {
if (n == 0 || n == 1) {
return n;
} else if (hm.containsKey(n)) {
return hm.get(n);
} else {
int temp = fibSum(n - 1) + fibSum(n - 2);
hm.put(n, temp);
return temp;
}
}
这个时候我们可以利用java的System.currentTimeMillis();计算这个方法运行所花的时间,比较前面两种写法的差异,时间减少了很多。然后我们来分析一下它的时间复杂度:由于hashmap当中已经把f(n-1)以下的都记录下来,所以都是直接取,画个二叉树就知道它的时间复杂度为O(n)。
然后,我们由前面这个例子来分析这个动态规划的模型:
三个重要概念:1.最优子结构 2.边界 3.状态转移公式
在前面的例子中 边界就是F(0)=0,F(1)=1,也就是递归出来的条件
状态转移方程式是F(n)=F(n-1)+F(n-2),也就是递归表达式。
最优子结构就是比如 F(8)=F(7)+F(6),这个例子其实不太好理解这个最优子结构,你可以理解为最优的解的意思,在背包问题中理解这个最优子结构是最容易的。
然后我们来看真正的动态规划是怎么实现的:
private static int fibSum(int n) {
if (n == 0 || n == 1) {
return n;
}
int a = 0, b = 1, temp = 0;
for (int i = 2; i < n; i++) {
temp = a + b;
a = b;
b = temp;
}
return a + b;
}
这里没有使用递归,而是使用迭代的方法实现。关键在于那个循环。这种方法时间复杂度依然为O(n),而空间复杂度变为了O(1),前面使用hashmap的空间复杂度为O(n).。这种方法只保存了虽然计算了所有的状态,但是只计算值的时候只使用了前面的两个状态,所以它只保存了前面的两个状态,然后迭代下去。用小灰的说法就是自底而上。我看过《算法图解》里对背包问题的分析,所以很容易理解这种思想。
这就是动态规划,实现了时间和空间上的最优。
这里的动态规划是有两种形式,一种是前面说的自底而上的模式,一种是我们说的第二种:用hashmap存储已经计算过的表达式,他们说是自顶而下的备忘录模式。为了避免误解,我觉得把两种观点都写出来比较好,反正都是一种思路,学会了就好。
欢迎交流和讨论。