数据结构学习-递归与动态编程

                                                                   数据结构学习-递归与动态编程
  前面我还刚刚在学习递归时拿Fibnoacci数列的递归实现做递归学习的例子呢,今天看到<<算法I-IV-基础,数据结构,排序与搜索>>的动态编程一节时,有这么一段话:
  下面是一个Fibonacci数列的递推的直接递归实现。千万不要使用这样的程序,因为它极端低效。实际上,对于计算F(N)的递归调用的数目正是F(N+1),但是,F(N)大约是1.618的N次方,令人时移尴尬的事实是那个递归程序(见上一篇日志)是针对这个微不足道的计算的指数级算法。 

  然后他给出了用数组实现的一个计算N个Fibonacci数的非递归程序,接着说实际只需跟踪2个值就可以了,因为最后得到F(N)只需前两个值,那就正好跟我上面写的非递归程序一致。接着他还提到了int 型也就是32位整数可以存放的最大Fibonacci数为F(45)=1836311903然后我用自己的程序算得的是F(46)才是这个值,估计我和他数第1个不一样,我以 1,1,2做为前3个,这个应该是没错的,然后我计算F(47)时就溢出成负数了,呵呵,我是拿非递归的那个版本计算的,然后我拿递归版本一算,哇靠,算个F(46)它竟然在我1.5GHz,256MB内存的机器上算了2分多钟,期间因为递归的函数调用要保存大量的信息入栈导致内存不够,频繁交换页面文件,硬盘灯狂闪,CPU占用率100%别的什么都动不了。我晕死,先头还没拿老人家的话当回事,这下算是领教了。而我前一次用非递归求的时候一秒钟估计都没用到。

  当然他要是没有解释递归在这儿低效的原因,那他算是枉做Donald Knuth的学生了,呵呵,原因是:
在将整个问题分解为两个部分时,后一部分完全没有注意到它要用到的信息已经被计算出来了,而做了大量的重复计算。自己举个比如F(5)画出递归树,一下子就看出来了。

  指出了问题,解释了原由,当然还要给出改进的方法,方法就是运用动态编程(我总觉得这个名词用得有点夸张)
  先给出Fibonacci数列的动态编程解法:

int  F(int i)
{
 static int knownF[maxN];
 if(knownF[i]!=0) return knownF[i];
 int t=i;
 if(i<0) return 0;
 if(i>1) t=F(i-1)+F(i-2);
 return knownF[i]=t;
}
  用这个函数也跟非递归的版本一样,F(46)瞬间搞定。

  递推是一个有整数值的递归函数,我们可以按从最小开始之顺序计算所有函数值来求任何类似函数的值,在每一步使用先前已计算的值计算当前值。我们称这种方法为自底向上的动态编程。假设我们能够保存所有先前计算的值,那么它能应用于任何递归计算。这是一个算法设计的技术,我们必须注意一个简单的技巧,以便能从指数级向线性改进一个算法的运行时间。
  自顶向下的动态编程甚至是一个更简单的技巧,与自底向上的动态编程相比,它在同一(或更小)代价的基础上自动允许我们实现递归函数。我们实现递归程序存储每一个它所计算的值(正如它最末的步骤)。上面的改进版Fibonacci程序就是自顶向下的动态编程将运行时间减少为线性的例子。自顶向下的动态编程有时也称为默记法(memoization)。
  接下来他还举了个更复杂一点的例子--背包(kanpspack)问题,以前就总听说这个概念,今天一看,其实也不是很复杂,但是很典型,总之应该是个最优化决策的问题。用递归实现群举最终找出最佳策略。但这个算法中也存在跟Fibonacci数列相同的问题,当然同样也可以用动态编程解决之。
  背包问题的简单递归实现:
就如我们前面所说,千万不要使用这个程序,因为它将花费指数级时间,即使是对于小问题也不能完成。但是它毕竟表明了一种我们可以轻易改进的紧凑方案。

先有包定义:
typedef struct{
int size;
int val;
}Item;

  然后有一个类型为Item的N项的数组,对于每一个可能的项,我们(递归地)计算我们所能得到的最大值,包括那一项,然后挑出那些值中最大的一项。(M为包的大小)

int knap(int cap)                      //cap==M
{
 int i,space,max,t;
 for(i=0,max=0;i<N;i++)
 {
  if((space=cap-items[i].size)>=0)
  {
   if(t=knap(space)+items[i].val)>max)
    max=t;
  }
 }
 return max;
}


背包问题的动态编程实现:

  由于我们简单地保存我们所计算的函数值,然后在我们需要的时候检索已经保存的值(使用一个wentinel值来表达未知值),而不是制造递归调用。我们存储数据项索引,以便我们能够在计算之后重建背包的内容,如果我们希望:itemKnown[M]在背包中,那么剩下的内容就跟大小为M-Known[M].size的最优的背包的内容一致,因此itemKnown[M-items[M].size]在背包里,以此类推:

int knap(int M)
{
 int i,space,max,maxi=0,t;
 if(maxKnown[M]!=unknown) return maxKnown[M];
 for(i=0,max=0;i<N;i++)
 {
  if((space=M-items[i].size)>=0)
  {
   if((t=knap(space)+items[i].val)>max)
   {
    max=t;
    maxi=i;
   }
  }
 }
 maxKnown[M]=max;
 itemKnown[M]=items[maxi];
 return max;
}

  在自顶向下的动态编程中,我们存储已知的值;在自底向上的动态编程中,我们预先计算这些值。我们通常选择自顶向下的动态编程而不选择自底向上的动态编程,其原因如下:
 自顶向下的动态编程是一个自然问题解决方案的机械转换;
 计算子问题的顺序能自己处理;
 我们或许不需要所有子问题的答案。
动态编程的应用程序在子问题的本质以及我们关于子问题的要存储的信息总量是不同的。
 我们不能忽视的至关重要的一点是,当我们需要的可能的函数值的数目太高以至于不能存储(自顶向下)或预处理(自底向上)所有值时,动态编程就会变得低效。比如,如果背包问题中的M和数据项是64位或者是浮点数,我们将无法通过索引到数组中去存储值。这一差别不仅导致了小麻烦,还带来了一个主要困难。对这种问题,却没有好的办法,以后会看到,的确是没有好的办法。

 动态编程是一个算法设计技巧,基本适合我们以后要做的那些高级的排序,搜索问题。无论如何,自顶向下的动态编程确实是开发高效的递归算法实现的基本方法,这类算法应该归入任何从事算法设计与实现所需的工具箱。 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值