《算法导论》学习之旅-第十五章-动态规划

序言

书中介绍动态规划比较复杂,看得不是特别地懂,我将从我自己理解的动态规划来做一些记录和介绍。

什么是动态规划

在说动态规划之前,我们先谈一谈斐波那契数列。斐波那契数列是第n个元素和第n-1个和第n-2个元素之和即 f(n) = f(n-1)+f(n-2)(这个方程在DP里面也称为状态转移方程)。基于这个性质,我们很容易想到可以构建一棵递归二叉树:
《算法导论》学习之旅-第十五章-动态规划_第1张图片
假如我们使用递归的算法的话,我们将会得到一个指数级的时间,这是一个非常恐怖的事情,用代码是实现是这样的:

int fib( int n )
{
	if( n=0)
		return 0;
	if( n==1)
		return 1;
	return fib(n-1) + fib(n-2);
}

为什么会这样呢。其实我们可以发现,在这个树中,其实重复运算了很大一部分:
《算法导论》学习之旅-第十五章-动态规划_第2张图片
像这样,随着元素的增加,重复运算的次数,将会越来越大,就会造成一种指数级别的增长。我们就要思考如何去优化一下这种算法呢。其实问题是出在重复运算上面的。假如我们只让它计算一次的话,我们就可以将O(2n)降到O(n),这样我们就有了叫记忆化搜索的方法,就是设置一个标志量,这样就会减少重复的递归,这种方式也叫做是自上而下的解决办法:

arr = vector<int>(n+1, -1)

int fib( int n )
{
	if( n=0)
		return 0;
	if( n==1)
		return 1;
	if (memo[n] == -1)					//设置了一个标志量
		return fib(n-1) + fib(n-2);
	return memo[n];
}

而当然也有一种自下到上的解决方法,用来解决斐波那契数列数列的话可以这样写:

int fib(int n)
{
	vector<int> memo(n+1, -1);
	
	memo[0] = 0;
	memo[1] = 1;
	for (int i=2; i<=n; i++)
		memo[i] = memo[i-1] + memo[i-2];
	return memo[n];
}

所以可以解答什么是动态规划的问题了:

动态规划:将原问题拆解决成若干个子问题,同时保存子问题的答案,是每个子问题都只求解一次,最终获得原问题的答案

且动态规划通常用来求解决最优解的问题。根据算法导论,我们可以有四个步骤来设计一个动态规划算法:

  • 刻画一个最优解的结构特征
  • 递归地定义最优解的值
  • 计算最优解的值,通常采用自底向上的方法
  • 利用计算的信息构造一个最优解

动态规划的两种实现方法

对于上面提到的自顶向下和自下而上的解决方法就是动态规划的两种实现方法。

对于自顶向下的求解方法,我们称为带备忘的自顶向下法,这种方法还是按照递归的方式来写的,只是会用一个数组或者哈希表来保存每个子问题的解。当遇到相同的子问题的时候不会再进行重复求解,这个过程称之为带备忘的

而自下而上的方法叫做自底向上法,这个方法的话,相对来说要难一点,简单的说,就是需要找到一个恰当定义子问题的概念,使得任何子问题的求解都是依赖于更小问题的求解。

其实这两种方法得到的算法具有相近的渐近时间。

动态规划的一般解题方法

在力扣上面刷了几道题后发现,有些题跟斐波那契数列的优化过程是很像的,大多可以通过暴力递归(这种往往会付出较大的时间空间代价)→带备忘录的递归解法非递归的动态规划解法这样的过程。当然这是我们思考的方向,虽然有这种流程,当时并不代表就能够解决动态规划的问题。

根据《算法导论》里面说的动态规划的原理,解决动态规划问题我们就需要描述,解决最优子结构问题重叠问题。解决最优子结构的问题,我看的不是特别的懂,这里不做过多的阐述。但是重叠问题(递归算法会重复的求解同一个子问题)的解决,是通过一个记忆化功能或者说是备忘录,这种方式的实现就是用一个数组或者哈希表来存有标志量作为备忘录,就像是上面的斐波那契数列的设置以数组全部设置为-1,遇到了相同的子问题不会重复的求解而是直接的返回。

然后,解决动态规划问题的大致的步骤分为三步:

  • 建立状态转移方程
  • 缓存并复用以往结果
  • 按顺序从小往大算

在我遇到的题中,参考人家的解法,大多就是分成这几种步骤来分别完成的,但是其实并不简单。动态规划问题是一个大而难的问题,相当的灵活,不能够通过简单的几句话就能够理解得很透彻,要形成解决动态规划的思考方式,可以通过做一些题,总结其方法,寻找里面的规律。

你可能感兴趣的:(算法导论)