当前位置:首页 > 算法 > 正文内容

【算法】动态规划(三)——解题方法与解题思路

亿万年的星光4年前 (2021-10-10)算法1547

0.前言

动态规划最核心的思想就是:拆分子问题,记住过往,减少重复计算。

动态规划可以从下面的参考网上的一个例子:

A :"1+1+1+1+1+1+1+1 =?"
A :"上面等式的值是多少"
B :计算 "8"
A :  在上面等式的左边写上 "1+" 呢?
A : "此时等式的值为多少"
B :  很快得出答案 "9"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"


1.动态规划的两种基本形式

一般求解的方式有两种:①自顶向下的备忘录法自低向上

为了说明动态规划的这两种方法,举个简单例子:
【题目描述】已知斐波那契数列第n项。0,1,1,2,3,5,8.......输出斐波那契数列的第n项。

【输入描述】一个正整数n。表示第n项。

【输出描述】第n项是多少。

【样例输入】4

【样例输出】2

我们前面解过这个题目,很快可以写出递归函数来。

nt calculate(int n) 
{
    if(n==1)    return 0;//判断是否到达递归边界n=1
    else if(n==2)   return 1;//判断是否到达递归边界n=2
    else    return calculate(n-1)+calculate(n-2);//未到达继续递归
}

我们分析一下递归的流程,加入计算n=5


[图片=http:??]

上面的递归树中的每一个子节点都会执行一次,很多重复的节点被执行,calculate(2)被重复执行了3次。由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。


下面看看动态规划如何求解斐波那契数列问题?

①自顶向下——备忘录法

参考代码:

public class Solution {
//使用哈希map,充当备忘录的作用
		Map<Integer, Integer> tempMap = new HashMap();
		public int numWays(int n) {
// n = 0 也算1种
			if (n == 0) {
				return 1;
			}
			if (n <= 2) {
				return n;
			}
//先判断有没计算过,即看看备忘录有没有
			if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
				return tempMap.get(n);
			} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
				tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
				return tempMap.get(n);
			}
		}
}


备忘录法也是比较好理解的,创建了一个n+1大小的数组来保存求出的斐波拉契数列中的每一个值,在递归的时候如果发现前面fib(n)的值计算出来了就不再计算,如果未计算出来,则计算出来后保存在数组中,下次在调用fib(n)的时候就不会重新递归了。比如上面的递归树中在计算fib(6)的时候先计算fib(5),调用fib(5)算出了fib(4)后,fib(6)再调用fib(4)就不会在递归fib(4)的子树了,因为fib(4)的值已经保存在其中了。



②自底向上的动态规划

备忘录法还是利用了递归,上面算法不管怎样,计算fib(6)的时候最后还是要计算出fib(1),fib(2),fib(3)…,那么何不先计算出fib(1),fib(2),fib(3)…,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:

  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。

动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在题中:

  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

  • f(n)= f(n-1)+f(n-2)就称为状态转移方程

  • f(1) = 1, f(2) = 2 就是边界啦

  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。

我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:

int fibonacciDP2(int n) {       // 动态规划使用数组
    int *a = new int[n+1];
    
    a[0] = 0, a[1] = 1;
    for (int i = 2; i < n+1; i++) {
        a[i] = a[i - 1] + a[i - 2];
    }
    return a[n];}

动态规划的解题套路

什么样的问题可以考虑使用动态规划解决呢?

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。

比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。

动态规划的解题思路

动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于问题,动态规划的思路:

  • 穷举分析

  • 确定边界

  • 找出规律,确定最优子结构

  • 写出状态转移方程


简单模板:

dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
    for(状态2 :所有状态2的值){
        for(...){
          //状态转移方程
          dp[状态1][状态2][...] = 求最值
        }
    }
}


扫描二维码推送至手机访问。

版权声明:本文由青少年编程知识记录发布,如需转载请注明出处。

分享给朋友:

相关文章

【贪心】区间覆盖

【贪心】区间覆盖

【题目描述】给定一个长度为m的区间,再给出n条线段的起点和终点(本题考虑闭区间),求最少使用多少线段可以将整个区间完全覆盖。【输入】第一行是区间长度m。第二行是n,表示有n个可选区间。后面跟着n行数据...

【算法】归并排序

【算法】归并排序

【参考代码】void msort(int s, int t){ if(s==t) return ;  //如果只有一...

【算法】高精度(2)

五、高精度处理进位与借位    其实也很简单,我们再来模拟一下,1439+887的时候,首先我们最低位相加,得到16,那么答案最低位就是6,再进个1,然后两数的十位相加,...

【算法】广度优先搜索算法(BFS)

【算法】广度优先搜索算法(BFS)

一、广度优先搜索的过程    广度优先搜索算法(又称宽度优先搜索算法,BFS)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra...

【算法】单调栈

一、单调栈单调栈(Monotonic Stack)是一种特殊的栈结构,其核心思想是维护栈内元素的单调性(单调递增或单调递减)。单调栈通常用于解决与元素大小关系相关的问题,例如:找到数组中每个元素的下一...

【算法】动态规划(一)

1.基本概念在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖...