hdoj2044 一只小蜜蜂 以及如何让水题不水

这个话题的起因是这样的:

按照动态规划(DP)——入门篇的指示,我开始做入门所需的“先修课程”递推求解专题练习(For Beginner)。作者说题目很简单,也就是大家常说的“水题”。我的看法是,在打好基础之前,一定要认真对待每一道“水题”,因为这些“水题”是构成复杂问题的部件。复杂问题无一例外是由数个简单问题耦合而成的,假如你已经有能力把复杂问题分解为简单问题(Divide),你肯定不希望在挨个解决这些简单问题(Conquer)的时候卡壳吧?若不然,你一定会很懊悔。

因此,在打基础的阶段,一定要把“水题”做好、做透。下面试举一例,大言不惭贻笑大方~~~


hdoj 2044 一只小蜜蜂

题目的意思很简单,如下图(其中0<a<b<50):


分析:
设f(a, b)为a-->b的路径总数
  1. 显然f(a, a+1)=1, f(a, a+2)=2。
  2. 欲求f(a, b)。要达到b,最后一步不是从b-1过去的,就是从b-2过去的(b-2-->b-1-->b那也是从b-1过去的),即,f(a, b) = f(a, b-1) + f(a, b-2)。
  3. 所以应该递归求解,2为递归式,1为递归出口。
三下五除二把代码写完,提交:
#include <cstdio>

int f(int a, int b)
{
	//递归出口
	if (a+1==b)	return 1;
	if (a+2==b) return 2;

	//递归调用
	return (f(a, b-1)+f(a, b-2));
}

int main()
{
	int n, a, b;
	scanf("%d", &n);
	for (int i=0; i<n; ++i)
	{
		scanf("%d%d", &a, &b);
		printf("%d\n", f(a, b));
	}

	return 0;
}
结果... Time Limit Exceeded

其实也很好理解为什么会超时,因为 太多重复计算了,举个例子:
f(3, 9) = f(3, 8) + f(3, 7);
递归下降一步后,
f(3, 8) = f(3, 7) + f(3, 6);
f(3, 7) = f(3, 6) + f(3, 5);
放眼望去,计算f(3, 8)的时候计算了一次f(3, 7),下一步还要重复计算一次f(3, 7);计算f(3, 8)的时候计算了一次f(3, 6),计算f(3, 7)的时候又要计算f(3, 6)...

另一方面,我们来看看f(a, n)=f(a, n-1)+f(a, n-2)这个递归式的时间复杂度:T(n)=2T(n-1)+O(1), 一个问题分裂为两个几乎同样规模的问题,不用说,复杂度为 O(2^n)指数时间,这算法必须不能用。事实上,用这个算法计算f(1, 49),在我的pc上运行了5分钟还没出结果,我估计运行几天能不能出结果都是个问题。

既然太多重复计算导致算法在实际中不可行,必须 避免重复计算,实际上这也是 优化的本质。怎么避免呢?目前我知道 两种方案
  1. 把已经计算过的存起来,用到的时候不需要再算,直接取出来用即可。
  2. 参照计算斐波那契数列的非递归优化(其实这题的答案集合就是斐波那契数的集合),自底向上计算。尾递归转化为循环,本质同1。

好的我们先来试试 第2种方案: 自底向上计算不用递归用循环
#include <iostream>
using namespace std;

long long f(int a, int b)
{
	int step=b-a;

	if(step==1)	return 1;
	if(step==2) return 2;

	//下面是step>=3的情况,前两个步差的情况已经计算好了
	long long pre=2;
	long long prepre=1;
	long long sum;

	while (step-2)//此循环自底向上计算
	{
		sum=prepre+pre;

		prepre=pre;
		pre=sum;

		--step;
	}

	return sum;
}

int main()
{
	freopen("D:\\in.txt", "r", stdin);
	freopen("D:\\out.txt", "w", stdout);

	int n, a, b;
	long long ans;
	cin>>n;
	for (int i=0; i<n; ++i)
	{
		cin>>a>>b;
		ans=f(a, b);
		cout<<ans<<endl;
	}

	return 0;
}
一番 折腾(主要折腾在于采用int数据类型 溢出,然后又搞不定64位整型的输入输出格式,后来发现其实只要定义成long long,用iostream的cin、cout就好),终于ac了:

那是不是递归就不能用了呢?非也,我们还有第1种方案:将已经计算过的结果储存起来,用到的时候再取出,避免重复计算。
long long mymap[]数组记录坐标差值走法数目的关系,自然有mymap[1]=1, mymap[2]=2,其余值初始化为0。
#include <iostream>
using namespace std;

#define MAXN 50

long long mymap[MAXN];	//用于存储已经计算过的数据,用之前mymap[1]初始化为1,mymap[2]初始化为2,其余的元素初始化为0

long long f(int step)
{
	if(mymap[step]!=0)	//如果“下降”到已经计算好的数据,直接用起来
		return mymap[step];
	else
	{
		long long ans=f(step-1)+f(step-2);	//否则开始递归下降
		//在回溯的过程中把计算好的ans写入映射表
		mymap[step]=ans;

		return ans;
	}
}

int main()
{
	freopen("D:\\in.txt", "r", stdin);
	freopen("D:\\out.txt", "w", stdout);

	int n, a, b;
	cin>>n;
	for (int i=0; i<n; ++i)
	{
		//初始化映射表
		for (int j=0; j<MAXN; ++j)
		{
			mymap[j]=0;
		}
		mymap[1]=1;
		mymap[2]=2;

		cin>>a>>b;
		long long ans=f(b-a);
		cout<<ans<<endl;
	}

	return 0;
}
写好之后提交,果然ac了:

写完这个递归版本之后,感觉对递归的“下降”和“回溯”过程理解更深刻了。

最后上网一搜,此题还有dp做法,待dp入门之后补上。




你可能感兴趣的:(递归,超时,递推,一只小蜜蜂,hdoj2044)