迭代是一种不断用变量的旧值推出新值的过程。例如,程序设计中常用到的计数cnt=cnt+1(或cnt++),就是用变量cnt的值加上1后赋值给cnt;对k的求和s=s+k,就是用变量s的值加上k后赋值给s。这种用变量cnt、s的新值取代旧值的过程,实际上就是迭代。
递推实际上也是根据递推关系式不断推出新值的过程,与迭代有很多共同之处。很多迭代过程可以应用递推来解决;反过来,很多递推过程也可以应用迭代来解决。
例如,下面的水手分椰子问题,既可以采用递推法求解,也可以用迭代法求解。
【例1】水手分椰子
五个水手来到一个岛上,采了一堆椰子后,因为疲劳都睡着了。一段时间后,第一个水手醒来,悄悄地将椰子等分成五份,多出一个椰子,便给了旁边的猴子,然后自己藏起一份,再将剩下的椰子重新合在一起,继续睡觉。不久,第二名水手醒来,同样将椰子等分成五份,恰好也多出一个,也给了猴子。然后自己也藏起一份,再将剩下的椰子重新合在一起。以后每个水手都如此分了一次并都藏起一份,也恰好都把多出的一个给了猴子。第二天,五个水手醒来,发现椰子少了许多,心照不宣,便把剩下的椰子分成五份,恰好又多出一个,给了猴子。问原来这堆椰子至少有多少个?
(1)编程思路1。
应用递推来求解,按时间来实施递推。
设第i个水手藏椰子数为y(i)(i=1、2、…、5)个,第二天5个水手醒来后各分得椰子为y(6)个,则原来这堆椰子数为
x=5*y(1)+1
1)如何求取y(1)呢?
由于第二个水手醒来所面临的椰子数为4y(1),同时也为5y(2)+1,于是有
4*y(1)=5*y(2)+1
同样,y(2)与y(3)之间的关系为:4*y(2)=5*y(3)+1
一般地,有递推关系:4*y(i)=5*y(i+1)+1 (i=1、2、…、5)
2)递推的初始(边界)值如何确定?
问题本身没有初始(边界)条件限制,只要求上面5个递推关系式所涉及的6个量y(i)都是正整数。也就是说,若有6个整数y(i)满足5个方程4*y(i)=5*y(i+1)+1 (i=1,2,…,5),即为所求的一个解。
3)采用顺推法求解。
将递推式变形为从y(i)推出y(i+1)的形式
y(i+1)=(4*y(i)-1)/5 (i=1,2,…,5)
首先y(1)赋初值k后推出y(2),由y(2)推出y(3),…,依此经5次递推得y(6)。如果某一次推出的不是整数,则中止继续往后推,k增1后赋值给y(1),从头开始。
这样按时间顺序从前往后递推,若每次递推所得都是整数,则找到了解,打印输出。
为保证推出的y(i)为整数,则要求4*y(i-1)-1能被5整除(即前一个水手藏起一份后,剩下的4份能够给猴子一个,再被分成五份)。因此,可确定最小的k值为4,即y(1)赋初值4;若在递推过程中,某次y(i)不为整数,则重新赋y(1)从头再来,为保证4*y(1)-1能被5整除,因此 k 的增量可设置为5。
(2)源程序1。
#include <iostream>
using namespace std;
int main()
{
int i,k,x,y[7];
k=4; y[1]=k;
i=2;
while (i<=6)
{
if ((4*y[i-1]-1)%5!=0)
{
k=k+5; y[1]=k; i=2; // 若y(i)不是整数,k增1重试
}
else
{
y[i]=(4*y[i-1]-1)/5; // 递推求后一个水手藏起的椰子y(i)
i++;
}
}
x=5*y[1]+1;
cout<<"原有椰子至少"<<x<<"个。"<<endl;
for (i=1; i<=5; i++)
cout<<"第 "<<i<<" 个水手面临椰子 "<<5*y[i]+1<<" 个,藏 "<<y[i]<<"个。"<<endl;
cout<<"最后一起分时有椰子 "<<5*y[6]+1<<" 个,每人分得"<<y[6]<<"个。"<<endl;
return 0;
}
(3)编程思路2。
采用倒推法求解,即改为y(6)赋初值k后递推出y(5),由y(5)递推出y(4),依此经5次递推得y(1),“由后向前”递推式为:
y(i)=(5*y(i+1)+1)/4 (i=1、2、…、5)
为确保从y(6)推出整数y(5),显然y(6)(即初始参数k)只能取3、7、11、…,即取k%4==3。因而k赋初值为3,k的增量为4。
(4)源程序2。
#include <iostream>
using namespace std;
int main()
{
int i,k,x,y[7];
k=3; y[6]=k;
i=5;
while (i>=1)
{
if ((5*y[i+1]+1)%4!=0)
{
k=k+4; y[6]=k; i=5; // 若y(i)不是整数,k增1重试
}
else
{
y[i]=(5*y[i+1]+1)/4; // 递推求前一个水手藏起的椰子y(i)
i--;
}
}
x=5*y[1]+1;
cout<<"原有椰子至少"<<x<<"个。"<<endl;
for (i=1; i<=5; i++)
cout<<"第 "<<i<<" 个水手面临椰子 "<<5*y[i]+1<<" 个,藏 "<<y[i]<<"个。"<<endl;
cout<<"最后一起分时有椰子 "<<5*y[6]+1<<" 个,每人分得"<<y[6]<<"个。"<<endl;
return 0;
}
在思路(1)中,采用顺推法,从前向后推,即从大到小推,试到k=3124才完成,从k=4到k=3124,试了625次;在思路(2)中,采用倒推法,从后往前推,即从小往大推,只要试到k=1023即可完成,从k=3到k=1023,试了256次。可见,在应用递推时,选用合适的递推方向关系到递推的效率。
(5)编程思路3。
用迭代法求解。
从最后5位水手一起分椰子时的椰子数residual入手,设residual的初始值为6(每个水手至少能分1个,丢1个给猴子),但这不可能,因为residual的值一定是第5位水手分成5份后,藏1份,剩下的4份,即每次剩下的一定是4的倍数,因此residual值一定满足两个条件:(1)是4的倍数;(2)减1后能被5整除。即residual的值为16、36、56、76、…。
对residual值向前推导。看看能否前推5次,且每次剩下的椰子数均是4的倍数。例如,当residual=16时,第5位水手面临的椰子数应为peachNum=present/4*5+1=16/4*5+1=21,而第5位水手面临的椰子数是第4位水手藏起1份后剩下的4份,显然21不是4的倍数,因此residual=16不可行,修改residual的值,使residual=residual+20=36,重新推导。
迭代时,迭代初值为 present=residual,迭代关系式为peachNum=present/4*5+1, present=peachNum,迭代控制条件为:在保证每次迭代后,present的值为4的倍数的情况下,迭代次数能达到5次。若迭代过程中,得到的present的值不是4的倍数,则修改residual的值,使residual=residual+20=36,重新迭代求解。
(6) 源程序3。
#include <iostream>
using namespace std;
int main()
{
int residual,present,peachNum,count;
residual=16;
count=0;
present=residual;
while (count<=4)
{
if(present%4!=0)
{
count=0;
residual+=20;
present=residual;
}
peachNum=present/4*5+1;
count++;
present=peachNum;
}
cout<<"原有椰子至少"<<peachNum<<"个。"<<endl;
return 0;
}
比较递推与迭代,两者的时间复杂度是相同的。所不同的是,递推往往设置数组,而迭代只要设置迭代的简单变量即可。
递推过程中数组变量带有下标,推出过程比迭代更为清晰。也正因为递推中应用数组,因此保留了递推过程中的中间数据。例如,每个水手i藏起的椰子都保存在数组y[i]中,随时可以查看;而迭代过程中不保留中间数据。