动态规划

动态规划是一种算法思想,将原始问题拆分成规模更小且与原始问题性质相同的子问题,利用子问题的解得到原始问题的解。

动态规划的适用条件之一是存在重叠子问题。动态规划的实现方式可以是自顶向下或自底向上。

    1)使用自顶向下实现时,通常使用递归实现,但这种方式通常会重复计算相同的子问题,导致时间复杂度很高。

    2)为了充分利用重叠子问题的性质,需要存储已经计算得到的子问题的答案,这样下次在遇到相同子问题的时候就可以直接得到答案

       而不需要重复计算。这就可以使用自底向上,通常使用迭代实现,此时可以确保每个子问题只会被计算一次而不会被重复计算。

语言是苍白的,下面来看几个例子:

1. 斐波那契数列

   数列形如:$1,1,2,3,5,8,...$,满足如下的递归式

$$fib(n) = \left\{\begin{matrix}
1, & n = 1 \; or \; n = 2\\
fib(n-1) + fib(n-2), & other
\end{matrix}\right.$$

   我们用树的结构来描述这个问题是如何拆分成一系列子问题的,或者说用树的形式来描述一下这个递推程序。

   以数列:$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];
}  

 

posted @ 2020-09-20 13:02  _yanghh  阅读(427)  评论(0编辑  收藏  举报