动态规划之重叠子问题案例

动态规划(Dynamic Programming)

  运筹学的一种最优化方法,常用于计算机应用,如求最长递增子序列,最小编辑距离。

1. 问题形式

求最值

核心方法: 穷举

  • 列出所有可行解
  • 找最值

2. 问题特点

状态、选择、dp数组定义

  • 问题存在”重叠子问题“
      暴力穷举效率低下,需要通过”备忘录“或者”DP table“优化穷举。
  • 问题具备”最优子结构“
      通过子问题最值得到原问题最值
  • 问题的”状态转移方程“
      第一:问题最简单的情况是什么?
      第二:问题有哪些”状态“?
      第三:对于每个”状态“,有哪些”选择“使得
         ”状态“发生改变?
      第四:如何定义dp数组/函数来表现”状态“和”选择“?

3. 案例

3.1重叠子问题(斐波那契数列)

在这里插入图片描述
3.1.1 暴力递归(提出问题)
斐波那契数列数学形式:递归
代码:

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

上述代码:
优点:简单易懂
缺点:低效
分析:假设n=20时,它的递归树为:
在这里插入图片描述求原问题fib(20)值:
  (1)先计算子问题fib(19)和fib(18)
  (2)然后计算fib(19)
     需计算fib(18)和fib(17)
     …
     以此类推
     计算fib(1)和fib(2)
  (3)返回结果,递归树不向下生长。
递归算法时间复杂度计算:

子问题个数 * 一个子问题需要的时间

  • 子问题个数=递归树中节点的总数
      因为二叉树节点总数为指数级别,所以子问题个数的时间复杂度为O(2n)。
  • 一个子问题需要的时间
      因为无循环,只有fib(n-1)和fib(n-2)的加法操作,所以时间复杂度为O(1)。

综上:时间复杂度O(2n)(即两者之积)

   通过观察递归树,算法低效的原因:大量重复计算。如fib(18)被计算两次。fib(18)为根的递归树体量巨大,多算一遍消耗大量时间,同时不止fib(18)这一个节点被重复计算。

上述重复计算的过程被称为重叠子问题

3.1.2 带备忘录的递归解法(解决问题)
  通过分析上述提出的重叠子问题,知道造成耗时的原因是重复计算,那么我们可以造一个”备忘录“,每次算出某个子问题的答案后先不要返回,而是先将其记到”备忘录“里再返回;每次遇到一个子问题先去”备忘录“里查一查,如果发现之前解决过这个问题,可以直接引用这个答案,不需要重复计算了。
  ”备忘录“:数组或者哈希表(字典)
代码:

		int fib(int N){
			if(N==0){
				return 0;
			}
			//将备忘录全部初始化为0
			vector<int>memo(N+1,0);
			//进行备忘录的递归
			return helper(memo,N);
		}


		int helper(vector<int>&memo,int n){
			//base case最简单的情况
			if(n==1||n==2){
				return 1;
			}
			//已经计算过的情况
			if(memo[n]!=0){
				return memo[n];
			}
			memo[n]=helper(memo,n-1)+helper(memo,n-2);
			return memo[n];
		}

分析:假设n=20时,“备忘录”的递归树:
在这里插入图片描述通过“备忘录”上述递归树进行了”剪枝“,改造成了一幅不存在冗余的递归图(其中为了方便书写fib()函数写成f()函数):
在这里插入图片描述递归算法时间复杂度计算:

  • 子问题个数=图中节点的总数
      因为不存在冗余计算,子问题就是f(1)、f(2)、f(3)…f(20),数量和输入规模N=20成正比,所以子问题个数为O(N)。
  • 一个子问题需要的时间
      因为无循环,只有f(n-1)和f(n-2)的加法操作,所以时间复杂度为O(1)。

综上:时间复杂度O(N)(即两者之积)

   与暴力算法相比,效率提高了许多

总结1:

   带“备忘录”的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了。
   带备忘录的递归解法解法:“自顶向下”
   动态规划:“自底向上”的。

啥叫“自顶向下”?

   我们刚才画的递归树(或者说图),是从上向下延伸的,都是从一个规模较大的原问题,比如f(20),向下逐渐分解规模,直到f(1)和f(2)这两个base case, 然后逐层返回答案, 这就叫“自顶向下”。

啥叫“自底向上”?

   与上述相反,我们直接从最下面、最简单、问题规模最小的f(1)和f(2)开始往上推,直到推到我们想要的答案f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算的关键所在。

3.1.3 dp数组的迭代解法(解决问题)
   有了上一步“备忘录”的启发,我们可以把这个“备忘录”独立出来成为一张表,就叫作DP table, 在这张表上完成“自底向上”的推算!

		int fib(int N){
			if(N==0){
				return 0;
			}
			if(N==1||N==2){
				return 1;
			}
			vector<int>dp(N+1,0);
			//base case
			dp[1]=dp[2]=1;  
			for(int i=3;i<=N;i++){
				dp[i]=dp[i-1]+dp[i-2];
			}
			return dp[N];
		}

DP table表如下:
在这里插入图片描述  DP table特别像之前那个“剪枝”后的结果,只不过反过来了。实际上带备忘录的递归解法中的”备忘录“最终就是这个DP table, 所以说这两种解法其实是差不多的, 在大部分情况下, 效率也基本相同。
  这里引出了“状态转移方程”这个名词,实际上就是描述问题结构的数学形式:
在这里插入图片描述

为哈叫“状态转移方程”?

  把f(n)想作一个状态n,这个状态n是由状态n-1和状态n-2相加转移而来,这就叫状态转移。

总结2:

  上面的几种解法中的所有操作, 例如语句return f(n-1) +f(n-2) ,dp[i] =dp[i 1-1] +dp[i-2], 以及对“备忘录”或DP table的初始化操作, 都是围绕这个方程式的不同表现形式,由此可见列出“状态转移方程”的重要性,它是解决问题的核心。而且很容易发现,其实状态转移方程直接代表着暴力解法。
  动态规划问题最困难的就是写出暴力解法,即状态转移方程。只要写出暴力解法, 优化方法无非是用“备忘录"或者DP table。
  在这个例子的最后,有一个细节的优化。根据斐波那契数列的状态转移方程, 当前状态只和之前的两个状态有关, 其实并不需要那么长的一个DP table来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为O(1);

		int fib(int N){
			if(N==0){
				return 0;
			}
			if(N==1||N==2){
				return 1;
			}
			int pre=1,curr=1;  
			for(int i=3;i<=N;i++){
				int sum=pre+curr;
				pre=curr;
				curr=sum;
			}
			return curr;
		}

  这个技巧就是所谓的“状态压缩”, 如果我们发现每次状态转移只需要DP table中的一部分, 那么可以尝试用状态压缩来缩小DP table的大小, 只记录必要的数据, 上述例子就相当于把DP table的大小从N缩小到2。 一般来说是把一个二维的DP table压缩成一维, 即把空间复杂度从O(N2) 压缩到O(N)。
  关于涉及动态规划的另一个重要特性“最优子结构”?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在说明重叠子问题的消除方法,演示为得到最优解法而逐步求精的过程。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Description 在讲动态规划课时,我们知道可用动态规划算法求解的问题应具备的一个基本要素是问题重叠性质,矩阵连乘问题能用动态规划求解正是因为它具有重叠问题。因此在解矩阵连乘问题的自顶向下的递归算法中,存在着大量的重叠问题计算。例如要计算4个矩阵A1A2A3A4最小连乘次数,要分别计算A1(A2A3A4)、(A1A2)(A3A4)和(A1A2A3)A4三种情况下的最小连乘次数,而计算A1(A2A3A4)的最小连乘次数要计算其问题A2A3A4的最小连乘次数,A2A3A4最小连乘次数的计算有二种情况(A2A3)A4和A2(A3A4),它分别包括求A2A3和A3A4两个问题。同理,计算(A1A2)(A3A4)包含A1A2和A3A4两个问题;计算(A1A2A3)A4包含计算A1A2A3、A1A2和A2A3这三个问题。故在解A1A2A3A4的最小连乘次数时,其问题的计算和重叠次数分别是: A1A2计算2次,重叠1次;A2A3计算2次,重叠1次;A3A4计算2次,重叠1次;A1A2A3只计算1次;A2A3A4只计算1次;A1A2A3A4只计算1次。因此,4个矩阵A1A2A3A4连乘的重叠问题分别为:A1A2、A2A3和A3A4的计算各重叠一次。现在你的编程任务是:对于n个矩阵连乘,求其重叠问题的计算次数。 Input 第一行是1个整数n(2≤n≤300),表示有n个矩阵连乘,接下来一行有n+1个数,表示是n个矩阵的行及第n个矩阵的列,它们之用空格隔开. Output 输出重叠问题计算次数和对应的问题,中以空格隔开,各问题重叠次数输出分别以A1、A2、… An打头的次序依次输出,格式如样例所示。如没有重叠问题输出NO Sample Input 4 30 35 15 5 10 Sample Output 1 A1A2 1 A2A3 1 A3A4
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值