9.1数字三角形

9.1.1 问题描述与状态定义

数学三角形问题

重点内容概述与讲解

(1)数字三角形问题中的状态与状态转移方程:
 状态:d(i,j)表示从点(i,j)出发可以求得的最优解(就是可求得的最大值)。
 状态转移方程:d(i,j)=max(d(i+1,j),d(i+1,j+1))+a(i,j)。

(2)理论讲解上文内容:
 d(i,j)就是表示一种状态,“人” 如其名 其记录的值就是从i,j出发的最优解。
 而状态转移方程则是描述这一状态(从i,j出发能求得的最优解)与其他状态的联系。
 很容易理解的是,从i,j出发能求得的最优解等于从下一步出发能取得的最大值+此处的值,而下一步出发能取得的最大值就是状态d(i+1,j)和d(i+1,j+1)。

(3)其实在学习任何东西之初我们都没有必要甚至不可能学到底学完学透,所以我们将知识点大概的理解就好了,等后面结合实际应用会自动深入的理解。具体的“通俗讲解”在其后介绍代码后给出。要是想要系统科学的学习,可以学习我的博客内容并参考其中的科学内容:数学-动态规划🎈

(4)动态规划的核心是状态和状态转移方程(边界处理要小心,解决问题的话细节处理也是必要的)。

9.1.2记忆化搜索和递推

使用状态和状态转移方程求解问题

求解实际方案如下:

int solve(int i,int j)
{
	return a[i][j]+( i==n?0:max(solve[i+1][j],solve[i+1][j+1]) );
}

代码讲解:

 solve函数用来求解三角形问题中的某一状态,即求解由i,j出发时的能求得的最大和。
 理论上讲它是通过状态转移方程求得的,如果i==n(因为三角形存储于数组a的第0到第n-1行,所以到达第n行就是边界条件,应该结束转移,返回此处的最佳状态0(也可认为不存在) )。
 实际上讲它是通过递归实现的,因为计算状态转移方程时我们需要比较(i+1,j)和(i+1,j+1)的状态,而我们是没有这两个状态值的所以又要调用函数去递归的求解(i+1,j)和(i+1,j+1)的状态。
 回归到求解的问题-数学三角形,求解的就是从第0(计算机存储中)行第0列位置出发走到底路径的最大值,我们可以发现即d(0,0)就是从0行0列位置出发求得的最大和,所以只需要求d(0,0)即可。
 那么这个函数可以求得d(0,0)的值吗?虽然理论上是可以的,但现实中往往是需要注意许多细节的(比如边界情况等等)。
 答案当然是肯定的,那么为什么能呢?其实从任何位置i,j出发求d(i,j)它都会比较d(i+1,j)和d(i+1,j+1)而来选择下一步的决策,而我们没有下一步的数据只能通过递归求解得到,那么递归会在什么适合停止呢?我们貌似并没有看到结束递归的判断条件,其实不一定需要那样的条件,那样条件的本质是返回一个非需要递归的值(即不再递归,就返回了)。
 细心的读者可以发现,其实这里也有就是当 i= =n 时,会返回0而不再进行递归。其实很简单,就是从i,j一直向外扩散递归,一点出发两条路再扩展到两条再到四条再到八条路,直到遇到边界条件 i= =n 也即递归到了最后一层的下一层(不存在的一层),所有递归的路径在这里返回。也就是递归到不存在的层返回0然后退回调用它的最下层(i= =n-1)然后这最后一层返回各自的值即a[i][j]给(i= =n-2)层,n-2层的每一个d(n-2,j)都比较最后一层返回的d(n-1,j)和d(n-1,j+1)的值选择其中较大的值(选择路径的终点较大的),然后(i= =n-2)层退回到调用它的某一个(i= =n-3)层的函数然后继续比较它与它相邻的路径选择较大的值。
 从整体上来看,就是从某一个位置一直出发扩散分支路径(2,4,6,8…)直到遇到n层返回,从最外层的节点依次返回,其实是从最外层开始求最长路径,次外层每一个节点都有两个对应的最外层节点,每个最外层节点选择下一层值较大的那个,然后就形成了长度为2的路,然后次次外层选择的是次外层形成的长度为2的路(当然只会选择对应两个中路径较长的一条),慢慢聚合路径分数越来越少(每层少1)直到最后合拢到i,j位置就是从i,j位置出发所能取得的最大和(即最优解)。任何一个i,j位置的最优解d(i,j)都可以这样求出来,则原问题d(0,0)也可以这样求出来。直接递归的n层,调用关系树也有n层,一共2^n-1个节点,所以时间复杂度是O(2 ^n)。

记忆化搜索

话不多说,先上主菜

int solve(int i,int j)
{
	if(d[i][j]>=0)return d[i][j];
	return d[i][j]=a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1));
}

讲解:
 上述内容我们已经解决了数学三角形的问题,但是细心的读者会发现这样的简单递归的效率并不高,这样的直接递归方法计算状态转移方程效率往往十分低下。原因是因为相同的子问题被重复计算的很多次。比如对于某一d[i][j]与它同层相邻的有d[i][j+1],而d[i+1][j+1]都属于他们可以选择的下一条路,但在进行d[i][j]和d[i][j+1]的状态计算时都需要计算d[i+1][j+1]的值来做比较而确定选择的路,这时它们会分别进行递归求解d[i+1][j+1],这样读者会不会觉得只是一层没什么?其实影响太大了,比如第二层递归时就要计算第三层到最后一层的线路,而第二层的两个位置就要分别计算从此到最外层的路这重复计算的量是多么庞大!而且第三层还会重复计算,直到次外层都会重复计算!因此直接递归求解的效率往往十分低下!
 那么我们应该怎样高效的计算呢?相信读者都能猜出来,就是记忆化搜索!所谓记忆化,就是记录寻找的状态( 记录d(i,j) ),当我们计算出来一个状态的适合记录它并且solve函数时,如果需要用到这个状态的值那么我们就直接返回记录的值,那就不用再重复计算,即避免了重复计算!(即如果d(i,j)不为零计算过就返回其的值)
 例如上文数学三角形问题,我们计算第i层时的状态时任何两个相邻的状态都会递归计算重复的部分,我们说路线是两倍两倍的扩展延申的,但其实运行的时候是一条一条延申的,因为就像深度优先搜索一样嘛!看似两个递归一起进行,但熟悉递归的读者很容易就知道递归是先进行首先运行到的那个然后再运行另一个。其实就是说每一次从i,j出发先走i+1,j再走i+1+1,j,直到遇到最外层才返回,然后每次返回调用的函数才进行i+1,j+1的递归。即分路递归,即有顺序的计算,那我们说同一层的计算可能重复,当d(i,j)计算完了d(i+1,j+1)时只要记录了它,那么之后的d(i+1,j)在使用时就不用再重复计算了可以直接使用!因此避免了重复的计算!(其实就是牺牲了空间,换取了一定的时间)。时间复杂度O(n^2),因为要访问di,j,i和j处于1-n之间。
 细节处理:i==n时就返回0,我们要将n设置为不存在的层,即我们定义数组时要比原先数学三角形多一行。这样访问d[i][j]时数组才不会越界!其他就没有什么了。

递推

话不多说,先上菜!

int i,j;
for( j=1;j<=n;j++)d[n][j]=a[n][j];
for( i=n-1;i>=1;i--)
	for(j=1;j<=i;j++)
		d[i][j]=a[i][j]+max(d[i+1][j],d[i+1][j+1]);

讲解:
 想必大家都发现了状态转移方程就是求出所有状态的奥妙,那么由状态转移方程的形式我们会想到,是否能通过方程直接计算出结果呢?答案当然是肯定的。因为递归也不过是分层使用此方程计算(本质上也是一个个的计算)。所以我们尝试使用直接计算方程的形式来求解数学三角形问题。
 以上代码:我们考虑直接使用方程求解,那么从哪里开始呢?从(0,0)状态出发?我们已经分析过递归时问题的求解过程,实际递归也只有从最外层的下一层返回时开始计算,即从最外层开始计算,那我们不妨从最外层开始计算吧!
 所以代码首先初始化最外层(细节处理,最外层之后可以用状态转移,但开始的初始层要手动求解,其实其值就是最大解啦)。然后从i=n-1(次外层)到第一层(递推时数组又不是从0开始了,细节注意),然后依次往内层计算,每一层用上一层的数据直接比较计算。最后到第一层(总起点)计算出所有的d(状态)结束。时间复杂度O(n^2)。在多数条件下,递推法的时间复杂度时:状态总数每个状态的决策个数决策时间。当不同状态的决策个数不同时,需要具体问题具体分析。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仰望—星空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值