动态规划是一种算法思想,将原始问题拆分成规模更小且与原始问题性质相同的子问题,利用子问题的解得到原始问题的解。
动态规划的适用条件之一是存在重叠子问题。动态规划的实现方式可以是自顶向下或自底向上。
1)使用自顶向下实现时,通常使用递归实现,但这种方式通常会重复计算相同的子问题,导致时间复杂度很高。
2)为了充分利用重叠子问题的性质,需要存储已经计算得到的子问题的答案,这样下次在遇到相同子问题的时候就可以直接得到答案
而不需要重复计算。这就可以使用自底向上,通常使用迭代实现,此时可以确保每个子问题只会被计算一次而不会被重复计算。
语言是苍白的,下面来看几个例子:
1. 斐波那契数列
数列形如:$1,1,2,3,5,8,...$,满足如下的递归式
$$fib(n) = left{egin{matrix}
1, & n = 1 ; or ; n = 2\
fib(n-1) + fib(n-2), & other
end{matrix}
ight.$$
我们用树的结构来描述这个问题是如何拆分成一系列子问题的,或者说用树的形式来描述一下这个递推程序。
以数列:$1,1,2,3,5,8,13$ 为例,其递归树如下:
从图中可以看出,存在很多重叠的子问题,比如红色方框内的 $fib(5)$,虽然其中一条分支已经计算过了,但另一条分支仍会计算。
自上而下的实现方式如下,采用的是递归:
int fib(int n) { if(n > 1) { return fib(n - 1) + fib(n - 2); } else { return n; // n = 0, 1 时递归终止 } }
自下而上的实现方式如下,采用的是迭代:
#define MAXN 100 int fib(int n) { int F[MAXN] = {0}; F[0] = 0; F[1] = 1; for(int i = 2; i <= n; i++) { F[i] = F[i - 1] + F[i - 2]; } return F[n]; }
因为递归太耗堆栈了,效率不高,所以能用迭代的话,还是尽量使用迭代。
迭代就相当于是记忆化搜索,将子问题的解记录下来,需要的时候直接从内存中取。
2. 多任务最大收益问题
上面这张图中,横轴代表时间,每一个小长方形表示一个任务,一共有 $8$ 个任务,长方形上的红色数字代表任务的收入。
每个任务不能同时发生,比如做第 $8$ 个任务的时候,就没办法做第 $7,6$ 这两个任务。
问题是:怎么样选择任务能获得最多的收益?
可以从单个任务考虑,针对第 $i$ 个任务,只会有两种可能:选或不选。设 $opt(i)$ 表示:在任务 $1-i$ 中能获得最多收益的最优解。
设 $prev(i)$ 为前一个不与 $i$ 重叠的任务(如上右图),$v_{i}$ 为选择任务 $i$ 能获得的收益,则
1)最优解中包含第 $i$ 个任务:$opt(i) = v_{i} + opt(prev(i))$。
2)最优解中不包含第 $i$ 个任务:$opt(i) = opt(i - 1)$。
在任务 $1-i$ 中做选择,只会有上面这两种情况。这个问题的递推式如下:
观察红框部分,很明显这里出现了重叠子问题。这里使用自底向上的方法,从 $opt(1),opt(2),...$ 逐步计算到 $opt(8)$。
3. 不相邻子数列最大和
比如一个数列:$4,1,1,9,1$,现在要在这个数列里面选出一些数字,这些数字不能有相邻的数,问:选哪些数字能使它们的和最大?
以下面数组进行举例:
设 $opt(i)$ 表示下标从 $0-i$ 这组数据求解的最佳方案,则上面的问题就是要求 $opt(6)$。
还是将每个数单独考虑,对于第 $i$ 个数,它只有两种可能,即选或不选。
1)选择第 $i$ 个数,则 $opt(i) = arr[i] + opt(i - 2)$。
2)不选第 $i$ 个数,则 $opt(i) = opt(i - 1)$。
可以做出其递推树如下:
可以看出,存在重叠的子问题。
先来看一下递归的代码:
int Opt(int arr[], int i) { if (i == 0) { return arr[0]; } else if (i == 1) { return max(arr[0], arr[1]); } else { return max(Opt(arr, i - 2) + arr[i], Opt(arr, i - 1)); } }
但是用递归会产生很多的重叠子问题,计算效率低,所以接下来用自底向上实现:
#define MAXN 100 int Opt(int arr[], int i) { int dp[MAXN] = {0}; dp[0] = arr[0]; dp[1] = max(arr[0], arr[1]); for(int j = 2; j <= i; ++j) { dp[j] = max(dp[j - 2] + arr[j], dp[j - 1]); } return dp[i]; }