今天来说一个very牛逼的算法———动态规划。听名字就觉得很厉害,不错,它确实很厉害,很多问题只能用动态规划来解决。
那么什么是动态规划呢?
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。
我们先说一个例子-------数字三角形,这个问题应该大部分人都见过吧,就是一个由1~100的数字搭建一个等腰三角形,从最顶层下到最底层,只能向左下或右下走,问经过的数字的最大值和是多少?最多有100层。这个问题其实用递归的话,思路很简单(对于基础稍微好点的),就是看它的最下面或者右下面那个到底层的路径之和最大,然后再加上它本身,从第一行开始。递归思路就是这,我就不过多阐述了,可以在网上搜。其代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
#define Max 101
int N;
int D[Max][Max];
int MaxRose(int i, int j)
{
if (i == N) return D[i][j];
else
{
int x = MaxRose(i + 1, j);
int y = MaxRose(i + 1, j + 1);
return max(x,y)+D[i][j];
}
}
int main()
{
cin >> N;
for (int i = 1; i <= N; i++)
{
for (int j = 1; j <= i; j++)
{
cin >> D[i][j];
}
}
MaxRose(1, 1);
cout << MaxRose(1, 1) << endl;
return 0;
}
虽然怎么看起来简单,但是它的时间复杂度特别高,存在着大量的重复计算,比如说数组D[i][j]里面存的就是这个数字三角形,其中任意一个D[a][b]到底层的最大路径用到它一次就要调用一次这个函数,肯定越到中间越到下面的就被调用的越多,而且关键是调用过程是一模一样,经过计算,每一层要调用的函数都为2的n次方,一共n层那么一共要调用2的1次方一直加到2的n次方,假如一共有n层,那么它的时间复杂度就为2^n,100层的话就是2的100次方,汉诺塔问题2的64次方就已经不得了了,更别说这了,就算是计算机也很难算出来吧!所以这个问题不能只用递归,我们想一下,那个递归的方法存在大量的重复计算所以时间复杂度特别高,那么可以可以当它第一次算出来后就保存起来,下次可以直接调用,那么它就没有那么多的重复计算了。这样每个只用算一遍,那么它的时间复杂度就为n的平方,n最多为100,那么才10000,直接看代码:
#include<iostream>
#include<algorithm>
using namespace std;
#define Max 101
int N;
int D[Max][Max];
int maxrose[Max][Max];//这个数组就是用来存对应下标的数到底层的最大路径
int MaxRose(int i, int j)
{
if (maxrose[i][j] != -1) return maxrose[i][j];
if (i == N) return D[i][j];
else
{
int x = MaxRose(i + 1, j);
int y = MaxRose(i + 1, j + 1);
maxrose[i][j] = max(x,y)+D[i][j];
}
return maxrose[i][j];
}
int main()
{
cin >> N;
for (int i = 1; i <= N; i++)
{
for (int j = 1; j <= i; j++)
{
cin >> D[i][j];
maxrose[i][j] = -1;//初始化为-1,表示还没被算出来过。
}
}
MaxRose(1, 1);
cout << MaxRose(1, 1) << endl;
return 0;
}
其实这个函数还有在空间上进行优化,我们没必要要用一个二维数组来存放maxrose[i][j];这样会有将近一般的空间是浪费的,我们可以直接就不用数组来存放maxrose[i][j],直接就在它本身的数组里面把原来的D[i][j]替换成maxsore[i][j],因为D[i][j]用一次之后就用不到了,还不如直接用来存放有用的,再优化一点,我们其实可以不用递归来写,我们直接用for循环来底层进行递推,这样让代码我们看起来更加的明了,好理解一点,而且其实这样代码也可以跑的更快一点,因为递归要一直在栈区开辟空间。代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
#define Max 101
int N;
int D[Max][Max];
void MaxRose()
{
for (int i = N; i >= 1; i--)
{
for (int j = 1; j <= i; j++)
{
if (i != N)
{
D[i][j] += max(D[i+1][j],D[i+1][j+1]);
}
}
}
}
int main()
{
cin >> N;
for (int i = 1; i <= N; i++)
{
for (int j = 1; j <= i; j++)
{
cin >> D[i][j];
}
}
MaxRose();
cout << D[1][1] << endl;
return 0;
}
这样是不是好多了呢?
用动态规划的一般解题思路:
1、要用到动态规划的问题,我们一般就会先想到用递归,它也是适用于可以把一个问题可以分解为若干个子问题,子问题和它本身的形式相同,只不过规模变小了,把所有子问题都解决了,那么这个大问题也就解决了,子问题一旦求出来一个就会被保存一个。
2、确定状态,状态就是要解决的每个子问题的过程(我是怎么理解的),比如数字三角形,它的状态就是看它左下或右下哪个到底层的路径大,就用哪个加上它本身代表它本身到底层的最大路径,状态值就是这个这个值。
3、确定一些初始状态和边界状态的值,然后用已知的推未知的。很显然数字三角形的初始状态就是最后一层的值,最后一层到最底层的最大路径肯定就是它本身,然后用它们推上一层的。
4、确定状态转移方程,这就是通过已知的推未知的的方程,显然数字三角形的状态方程为:
能用动态规划的问题的特点:
1、具有最优子问题的结构性质,简单来说当题目问你……最……路径的问题可以首先考虑动态规划。
2、按我的话来说,就是最后的结果不会和每个子过程都有关,只是最优解的那若干个子系列有关,和其他的无关。
这就是我对动态规划的一些拙见,前面我也做了一些关于动态规划的题目,大家可以看看加深一下印象:
1、矩阵
2、最优包含
3、K倍区间
4、最长上升子序列
有哪不对可以评论区指出来哦!!!欢迎指正哈!