引言
青蛙跳台阶问题是动态规划里较简单的题型了,因为状态转移方程很好找到。
但上周末遇到一道青蛙跳台阶面试题,给我难住了。状态方程竟然一时半会想不到!虽然目前是推出状态转移方程了,但颇有感触,遂想就青蛙跳台阶这一类做个总结。
普通青蛙跳台阶
一只青蛙一次可以跳上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取值从1
到i-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则需要重新思考其计算逻辑(还是要以复杂度为代价)。
结语
你以为你会了动态规划!!!
但动态规划告诉你:
我自己都不敢说我懂我自己
保持谦虚!积极前进!
一看就会,一写就废。