你管这玩意叫动态规划!!!我们该重新认识一下动态规划了!!!

264 阅读4分钟

一. 前言

之前看了一篇特别好的推文,现在不清楚出自哪儿了,我自己用通俗易懂的语言表述一下这个经典的算法。

二. 动态规划算法的概念(what)

(1) 定义。

动态规划是运筹学的一个分支,是求解决策过程最优化的过程,被广泛的用于算法求最优解中(参考百度百科)。我自己的理解就是一个大问题可以化成若干个小问题,得到小问题 的解就可以构造出大问题的解,是一个自底向上的过程。通常有一个缓存数组来缓存重复的子问题的解,只要以后遇到就返回该值来提升算法的效率。

过程通常是:先求解最小的子问题,把结果存在表格中,在求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率

(2)和分治法的区别

分治法也是将问题化分为若干个子问题;它是自顶向下合并子问题的解,从而得到原问题的解。使用动态规划的条件就是具有 最优子结构,问题的最优解包含其子问题的最优解。

三. 为什么使用和学习(why)

提升我们的算法功底,解决多个复杂的数学问题;如斐波拉契问题,阶梯问题,背包问题,最短路劲问题等。

四. 使用场景(where)

一切拥有最优子结构的问题都可以用动态规划算法求解。

五. 常见名词解析

通常我们使用动态规划算法时,需要找出以下几个“名词”

  1. 状态转移方程 通常为左式随着右式变化的动态式。表述为: F(n) = F(n-1) + F(n-2)(以阶梯问题为例) ;

  2. 最优子结构 继续以上例论述:最优子结构为右式F(n-1)+F(n-2)

  3. 边界值 继续以阶梯问题论述,当n为1时 F(1) = 1(只能一次走完),F(2) = 2(要么一次走完;要么分两次,一次走一介);边界值为F(1)和F(2)。

六. 经典问题解析。

1. 阶梯问题

有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。请问走到阶梯顶部一共有多少种走法?

分析: 首先我们要走到10级阶梯走法:要么你走到了第8个阶梯或者走到了第9个阶梯,即F(9),F(8);那么只要求到第8个阶梯的走法有多少种,走到第9个阶梯的阶梯的走法就可以构造出F(10)得解了,即:F(10)=F(9) + F(8) , 同理F(9)=F(8)+F(7),F(8) = F(7)+F(6)...,可以得出状态转移方程:F(n) = F(n-1) + F(n-1)。 那么此题边界值为多少呢??? 当n=3时,F(3) = F(2) + F(1) , F(2)代表阶梯为2;只有两种走法,要么走一次一阶,共两次;要么走两阶,一次走完;边界值为F(1)和F(2), 过程如图所示:

结论:此题可以变式为有一座高度是n级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。请问走到阶梯顶部一共有多少种走法?

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

  • 最优子结构为:F(n-1)+ F(n-2)

  • 边界值为: F(1) = 1 F(2) = 2

最终公式如下

现在我们看下代码

  1. 第一种写法
#include "iostream"
using namespace std;

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

int main() {
  int n = 0;
  cout<<"请输入台阶数N"<<endl;
  cin >> n ;
  int result = getWay(n);
  cout<<"result is:"<<result<<endl;
  return 1;
} 

一般人我们的写法就是第一种,可以很容易看出有重复计算,F(8)=F(7)+F(6) F(7)=F(6)+F(5); 下面使用缓存数组改写:

#include "iostream"
#include <vector>
using namespace std;

// 缓存数组,元素数9999,初市值为0 
vector<int>Cache(9999, 0); 
int getWay(int n) {
  if(n == 1) return 1;
  if(n == 2) return 2;
  // 如果缓存数组里面没有该数,就缓存。 
  if(Cache[n] == 0) {
      Cache[n] = getWay(n-1) + getWay(n-2);
  } 
  return Cache[n];
}
int main() {
  int n = 0;
  cout<<"请输入台阶数N"<<endl;
  cin >> n ;
  int result = getWay(n);
  cout<<"result is:"<<result<<endl;
  return 1;
} 

以上写法可以很容易的看出时间复杂度为O(n^2),下面我们看下更时间复杂度为线性阶的写法O(n)

  1. 第二种写法
#include <iostream>
using namespace std;

/** 
 * 题目:现在有n阶楼梯;一个人有两种方式;每次上一阶阶梯;或者每次上两阶阶梯;请问n阶阶梯共有多少种方式? 
 *  由题可知可轻易得出状态转移方程:   F(N) = F(N-1) + F(N-2);
 *  最优子结构为: F(n-1) + F(n-2);
 *  它的边界值为: F(1) = 1   F(2) = 2(要么一次上一阶(分两次上);要么一次上两阶(需要一次); 
 * 
 **/

int getWays(int n) {
	if(n==1) return 1;
	if(n==2) return 2;
	int a=1, b=2;
	for(int i = 3; i<=n ; i++) {
		int temp = a + b;
		a = b;
		b = temp;
	}
	return b;
} 

int main() {
	int n = 0;
	cout<<"请输入台阶数N"<<endl;
	cin >> n ;
	int result = getWays(n);
	cout<<"result is:"<<result<<endl;
	return 1;
} 

运行结果如下

写的不对的地方,欢迎各位大佬提出改正。

-- 参考如下:

百度百科动态规划

C++ vector解析