青蛙跳台你真的会码?

引言

青蛙跳台阶问题是动态规划里较简单的题型了,因为状态转移方程很好找到。

但上周末遇到一道青蛙跳台阶面试题,给我难住了。状态方程竟然一时半会想不到!虽然目前是推出状态转移方程了,但颇有感触,遂想就青蛙跳台阶这一类做个总结。

普通青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法?

分析

在这里插入图片描述
如上图,我们假设青蛙站在第i级台阶上

那么,在青蛙一次只能跳1级/2级台阶上,它肯定是从第i-1级上跳1级上来的,或者从第i-2级台阶上跳2级上来的,不存在其他可能。

很明显,跳上第i级的方案数量等于跳上第i-1级的跳法数量+跳上第i-2级的跳法数量

符号说明
i第i级台阶
f(i)跳上第i级台阶的跳法数量

接下来我们就能写出状态转移方程了:

f(i)=f(i-1)+f(i-2)

有了状态方程,接下来就要考虑边界条件

台阶级数没有负数,在编写代码时,索引至少要从0开始。因此状态转移方程的i取值必须大于等于2。

但一般f(0)比较难以确定,因此我舍弃0索引,直接从1开始,那么状态方程中的i至少为3。

这样一来,f(1)与f(2)就是所谓的边界条件,需要我们自己去确定。

f(1)是青蛙跳1级台阶的跳法,只有1种跳法(1),因此f(1)=1;
f(2)是青蛙跳2级台阶的跳法,只有2种跳法(1;2),因此f(2)=2;

边界条件也确定了,那么动态规划的思路分析也结束了。

该问题的分析结果如下:

状态转移方程:f(i)=f(i-1)+f(i-2)  i>2
边界条件:f(1)=1,f(2)=2

code

int FrogJump1(int n){
	if(n<0)//输入非法
		return -1;
	vector<int> f(n+1,0);
	f[1] = 1,f[2] = 2;//边界条件
	for(int i = 3;i<n+1;++i)
		f[i] = f[i-1] + f[i-2];//状态转移方程
	return f[n];
}

这道题只需要前两个状态,因此代码可以进一步优化:

int FrogJump1(int n){
	if(n<0)//输入非法
		return -1;
	int f[]= {1,2,0};//边界条件
	for(int i = 3;i<n+1;++i){
		f[2] = f[1] + f[0];//状态转移方程
		f[0] = f[1];
		f[1] = f[2];
	}
	return n>2?f[2]:f[n];
}

进阶青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶,还可以跳上3级台阶,求该青蛙跳上一个 n 级的台阶总共有多少种跳法?

分析

其实这道题和上一道题几乎一样,继续利用上题的符号,不再多分析。

状态转移方程:f(i)=f(i-1)+f(i-2)+f(i-3)  i>3
边界条件:f(1)=1,f(2)=2,f(3)=3

code

int FrogJump2(int n){
	if(n<0)//输入非法
		return -1;
	vector<int> f(n+1,0);
	f[1] = 1,f[2] = 2,f[3] = 3;//边界条件
	for(int i = 4;i<n+1;++i)
		f[i] = f[i-1] + f[i-2] + f[i-3];//状态转移方程
	return f[n];
}

这道题只需要前三个状态,因此代码可以进一步优化:

int FrogJump2(int n){
	if(n<0)//输入非法
		return -1;
	int f[]= {1,2,3,0};//边界条件
	for(int i = 4;i<n+1;++i{
		f[3] = f[2] + f[1] + f[0];//状态转移方程
		f[0] = f[1];
		f[1] = f[2];
		f[2] = f[3];
	}
	return n>3?f[3]:f[n];
}

变态青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶,……,还可以跳上n级台阶,求该青蛙跳上一个 n 级的台阶总共有多少种跳法?

分析

状态转移方程:f(i)=2*f(i-1)  i>1
边界条件:f(1)=1

code

int FrogJump3(int n){
	if(n<0)//输入非法
		return -1;
	vector<int> f(n+1,0);
	f[1] = 1;//边界条件
	for(int i = 2;i<n+1;++i)
		f[i] = 2*f[i-1];//状态转移方程
	return f[n];
}

这道题只需要前一个状态,因此代码可以进一步优化:

int FrogJump3(int n){
	if(n<0)//输入非法
		return -1;
	int f = 1;//边界条件
	for(int i = 2;i<n+1;++i)
		f<<=1;//状态转移方程
	return f;
}

可退青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶,在青蛙跳往n级的过程中,青蛙有且仅有一次倒退1级的机会,求该青蛙跳上一个 n 级的台阶总共有多少种跳法?

这道变体有两个思路做:

思路1(计算中间状态):青蛙站在第i(i<=n)阶,计算来到第i阶的跳法就是考虑青蛙是怎么来到第i阶的(这种思路时间复杂度为O(N^2)

思路2(不计算中间状态):青蛙已经到达终点,假设它在第k(k<n)之前使用了后退机会,第k阶便成为了一个节点,第k阶之前与之后计算方法都是一样的(这种思路时间复杂度为O(N))

思路1分析

在这里插入图片描述
结合上图,青蛙站在第i阶(跳法F(i),普通青蛙跳台阶f(i)),有两个分支可以使青蛙站在第i阶:

1).未使用后退机会:也就是说,在到达第i阶之前并未使用后退机会,计算跳法的方法就和“普通青蛙跳台阶”一致,即F(i)=f(i-1)+f(i-2)

2).已经使用了后退机会:青蛙在第k阶使用了后退机会,那么来到第k阶的跳法必然等于f(k),。因为后退了一阶,青蛙站在了k-1阶,接下来要按照“普通青蛙跳台阶”的方法跳到第i阶,即青蛙还有f(i-k+1)种方法跳到第i阶,那么此时青蛙从第0阶跳到第i阶的跳法就是f(k)*f(i-k+1)。因为k<i,k取值从1i-1(因为第i阶是目的地,上去了不考虑再后退,第0阶也不能后退),即青蛙使用后退机会跳到第i阶的跳法为:
在这里插入图片描述
综合两种分支,青蛙来到第i阶的跳法共计:
在这里插入图片描述
上式就是这道题的“状态转移方程“(其实也不是,因为F之间似乎没有关系,何谈转移,若是不明白思路1,可直接跳思路2哦~)

边界条件:

f(1) = 1,f(2) = 2;
F(1) = 1,F(2) = 4;

code

int FrogJump4(int n) {
	if (n < 0)//输入非法
		return -1;
	if (n == 1)
		return 1;
	vector<int> f(n + 1, 0);//存储不倒退跳法
	vector<int> F(n + 1, 0);//存储总跳法
	f[1] = 1, f[2] = 2;//不倒退边界条件
	F[1] = 1, F[2] = 4;//总跳法边界条件
	for (int i = 3; i < n + 1; ++i) {
		f[i] = f[i - 1] + f[i - 2];//不倒退状态转移方程
		F[i] = f[i];//不倒退跳法
		for (int j = 1; j < i; ++j)//倒退跳法
			F[i] += f[j] * f[i - j + 1];
	}
	return F[n];
}

这道题F需要f的所有状态,而与F的状态无关,因此代码可以进一步优化:

int FrogJump4(int n) {
	if (n < 0)//输入非法
		return -1;
	if (n == 1)
		return 1;
	vector<int> f(n + 1, 0);//存储不倒退跳法
	f[1] = 1, f[2] = 2;//不倒退边界条件
	int F[] = { 1,4,0 }; //总跳法边界条件
	for (int i = 3; i < n + 1; ++i) {
		f[i] = f[i - 1] + f[i - 2];//不倒退状态转移方程
		F[2] = f[i];//不倒退跳法
		for (int j = 1; j < i; ++j)//倒退跳法
			F[2] += f[j] * f[i - j + 1];
	}
	return n>2?F[2]:F[1];
}

思路2分析

青蛙要跳到第n阶,在跳到第n阶之前,可以在第k(k<n)阶使用后退机会,第k阶之前青蛙跳法计算与普通青蛙跳台阶计算方法一致,使用了后退,站在第k-1阶,此时只能继续按照普通青蛙跳台阶的方式跳向第n阶。
在这里插入图片描述

1).跳到第k阶,跳法f(k)
2).从k-1到第n阶,跳法f(n-k+1)
因为k有多个取值,因为青蛙从0跳到第n阶的跳法为:
在这里插入图片描述

code

int FrogJump4(int n) {
	if (n < 0)//输入非法
		return -1;
	if (n == 1)
		return 1;
	vector<int> f(n + 1, 0);
	f[1] = 1, f[2] = 2;//边界条件
	for (int i = 3; i < n + 1; ++i)
		f[i] = f[i - 1] + f[i - 2];//不倒退状态转移方程	
	int Fn = f[n];//不后退
	for (int i = 1; i < n; ++i)//后退跳法
		Fn += f[i] * f[n - i + 1];
	return Fn;
}

两个思路个人感觉思路二还是比较容易明白,并且复杂度低,因此应该是最优解法。但是思路一也是有其意义的,若题意让列出到达每一阶的跳法,思路1只需返回F即可,而思路2则需要重新思考其计算逻辑(还是要以复杂度为代价)。

结语

你以为你会了动态规划!!!
但动态规划告诉你:
我自己都不敢说我懂我自己

保持谦虚!积极前进!
一看就会,一写就废。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值