参考:https://www.zhihu.com/question/39948290
https://baijiahao.baidu.com/s?id=1635388976060265522&wfr=spider&for=pc
乔治·桑塔亚纳说过,“那些遗忘过去的人注定要重蹈覆辙。”这句话放在问题求解过程中也同样适用。不懂动态规划的人会在解决过的问题上再次浪费时间,懂的人则会事半功倍。那么什么是动态规划?这种算法有何神奇之处?本文作者给出了初步的解答。
假设你正在使用适当的输入数据进行一些计算。你在每个实例中都进行了一些计算,以便得到一些结果。当你提供相同的输入时,你不知道会有相同的输出。这就像你在重新计算之前已经计算好的特定结果一样。
那么问题出在哪里呢?你之前计算某些结果的宝贵时间被浪费掉了。你可以通过保存之前的计算结果去轻易地解决这个问题。比如通过使用恰当的数据结构。举个例子,你可以将输入输出作为键值对映射保存起来。
那些遗忘过去的人注定要重蹈覆辙 ~ 动态规划
现在通过分析这个问题,我们可以将新的输入(或者不在数据结构中的输入)与其对应的输出存储下来。或者在字典中查找输入并返回相应的输出结果。这样当你在进行一些计算时,你可以检查数据结构中是否存在该输入,如果数据输入存在的话就可以直接获得结果。我们将与这种方法相关的技巧称作动态规划。
详解动态规划
现在让我们更详细地介绍动态规划。
简而言之,我们可以说动态规划主要用来解决一些希望找到问题最优解的优化问题。
一种可以用动态规划解决的情况就是会有反复出现的子问题,然后这些子问题还会包含更小的子问题。相比于不断尝试去解决这些反复出现的子问题,动态规划会尝试一次解决更小的子问题。之后我们可以将结果输出记录在表格中,我们在之后的计算中可以把这些记录作为问题的原始解。
举个例子,有n个阶梯,一个人每一步只能跨一个台阶或是两个台阶,问这个人一共有多少种走法?
以下是三种不同的动态规划解决方案:
递归求解:原始递归求解
自上而下:你从最顶端开始不断地分解问题,直到你看到问题已经分解到最小并已得到解决,之后只用返回保存的答案即可。这叫做记忆存储(*Memoization*)。
自下而上:你可以直接开始解决较小的子问题,从而获得最好的解决方案。在此过程中,你需要保证在解决问题之前先解决子问题。这可以称为表格填充算法(*Tabulation,*table-filling algorithm**)。至于迭代和递归与这两种方法的关系,自下而上用到了迭代技术,而自上而下则用到了递归技术。
假设n=10,显而易见从0到10的走法等于从1到10和从2到10走法的和,climb(0)=climb(1)+climb(2),也就是说climb(i)=climb(i+1)+climb(i+2),这就是这个问题的核心(好多文章都称为最优子结构)
我们先来个原始递归求解,来个递归求解
int climb(int x)
{
if (x == 10)
return 1;
if (x > 10)
return 0;
int s = climb(x + 1);
int t = climb(x + 2);
return s + t;
}
int main()
{
int q = climb(0);
}
我们可以发现原始递归求解会重复计算好多,大家不清楚的话最好画个树看看递归到底重复计算了多少,好多算法都和数据结构绑在一起,理解了树的结构,对算法有比较大的帮助。
第二种方法自上而下记忆存储求解:
int climbmaxR[10] = { -1 };
int climb(int x)
{
if (x == 10)
return 1;
if (x > 10)
return 0;
if (climbmaxR[x] != -1)
return climbmaxR[x];
int s = climb(x + 1);
int t = climb(x + 2);
int k = s + t;
climbmaxR[x] =k;
return s + t;
}
int main()
{
for (int i = 0; i < 10; ++i)
climbmaxR[i] = -1;
int q = climb(0);
return 0;
}
最后一种动态规划求解方法,自下而上求解。我们可以发现如果要求解climb(8),需要知道climb[9]和climb[10]。那我们可以先算出
climb[9]和climb[10]然后依次往上推即可。
int main()
{
int f[11];
f[10] = 1;
f[9] = 1;
for (int i = 8; i >= 0; --i)
f[i] = f[i + 1] + f[i + 2];
return 0;
}
追根溯源
Richard bellman 是这个概念的提出者。他在 20 世纪 50 年代中期为兰德公司工作时想到了这一点。选择「dynamic programming」这个名字的原因是为了隐藏他为这项研究所做的数学工作。因为他担心他的老板会反对或不喜欢任何类型的数学研究。
所以「programming」这个词只是一个参考,以表明这是一种老式的计划或调度方式,通常是通过逐渐填充表格(以动态方式而不是线性方式)而不是一次全部填入的方式进行。