1.动态规划算法

动态规划(dp)

	首先:动态规划问题一般的形式就是 求 最值 
			解 动态规划的核心问题 就是 穷举
	然后:动态规划穷举有点特别,因为这类问题存在 “重叠子问题” ,如果暴力穷举,效率会低下,所以需要借助备忘录“ dp table”来优化穷举过程,避免不必要的计算
	然后:动态规划 一般 会具备 “最优子结构” ,这样才通过子问题求最值

动态规划三要素

	重叠子问题、最优子结构、状态转移方程
		写状态转移方程 :
			1.这个问题的最简单状况(初始值)是什么
			2.这个问题有什么”状态“
			3.对于每个“状态”,可以做出的“选择”是什么才能使 其“状态” 改变
			4.如何定义 dp 数组 来表现“状态”和“选择”
		说白了就是三点:状态、选择、dp数组的定义。。。。

例题讲解 1、斐波那契数列(LeetCode 509)

第一种方式: 暴力递归
int fib(int num){  
    if(num==0){  
        return 0;  
    }  
    if(num==1||num==2){  
        return 1;  
    }  
    return fib(num-1)+num-2;  
}
	代码简单易懂,但是十分的低效,低效在哪里?
	假如 num=20; 画出递归树,如图


递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。

子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。

解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。

所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。

第二种方式:备忘录(动态规划)
		明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
int fib(int N) {  
	 if (N < 1) 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];  
}

这次再次 画图递归树 如图 :

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

递归算法的时间复杂度怎么算?****子问题个数乘以解决一个子问题需要的时间。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是f(1),f(2),f(3)f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。

解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n)。比起暴力算法,是降维打击。

第三种方式:dp数组的迭代解法
	有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,就叫做 DP table 吧,在这张表上完成「自底向上」的推算岂不美哉!
int fib(int N) {  
	 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];  
}

这里,引出「状态转移方程」这个名词,实际上就是描述问题结构的数学形式:

posted @ 2021-07-03 22:37  宋佳强  阅读(249)  评论(0编辑  收藏  举报