关于斐波那契的两道算法题
题目一:爬楼梯
假设你正在爬楼梯。需要n阶你才能到达楼顶。
每次你可以爬 1 或者 2 个台阶。有多少中不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例1:
输入:2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1阶 + 1阶
2. 2阶
示例2:
输入:3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1阶 + 1阶 + 1阶
2. 2阶 + 1阶
3. 1阶 + 2阶
题目分析——思考路线:
做这类题目,如果你是老司机,那么肯定可以直接往正确的思路上想了。
如果你是个小白,那么最好的办法就是先代入几个简单的值,来寻找规律。
当n=1,那只有一种,一步到位。
————————————————
当n=2,两种:
1. 1阶 + 1阶
2. 2阶
我们发现我们可以 1 步踏上2层,也可以一次 1 层,踏两步。
当n=3,三种:
1. 1阶 + 1阶 + 1阶
2. 2阶 + 1阶
3. 1阶 + 2阶
我们发现,在完成最后一步前,我们可能停留在第 1 阶,也有可能停留在第 2 阶。
既然已经说了是在完成 最后一步前 的位置,那么就分下面两种情况:
1. 最后一步前的位置,在第 1 阶,最后一步就跨 2 阶
满足这种情况的有上面的:
结果3: 1阶(最后一步前的位置) + 2阶
这种情况下结果只有1种,我们暂时没发现什么特别明显的规律,不要着急,继续看后面的情况,套用百遍,规律自现。
2. 最后一步前的位置,在第 2 阶,最后一步就跨 1 阶
满足这种情况的有上面的:
结果1: 1阶 + 1阶(最后一步前的位置) + 1阶
结果2: 2阶(最后一步前的位置) + 1阶
这种情况下的结果有两种,别动,别动,好好看看有啥发现没有?
细细观察后,我们发现了他们的共同点,那就是:最后一步是相同的都是跨 1 阶。
有共同点,就应该考虑不同点:踏上第 2 阶的方法数。
如此看来,最后一步,只是无论是跨 1 阶还是跨 2 阶,总的方法数就取决于最后一步前的位置的方法数之和,公式如下:
总方法数 = 最后一步前在第 1 阶方法数 + 最后一步前在第 2 阶方法数
到这里,是不是感觉脑子里面,有那么点意思了?
哈哈,不急,我们还需要验证一下,咱们继续往下代入,套用。
当n=4,五种:
1. 1阶 + 1阶 + 1阶 + 1阶
2. 1阶 + 1阶 + 2阶
3. 1阶 + 2阶 + 1阶
4. 2阶 + 1阶 + 1阶
5. 2阶 + 2阶
因为1步最多只能跨2阶,所以,最后一步前的位置只能在:
第 3 阶:
结果1: 1阶 + 1阶 + 1阶(最后一步前的位置) + 1阶
结果3: 1阶 + 2阶(最后一步前的位置) + 1阶
结果4: 2阶 + 1阶(最后一步前的位置) + 1阶
第 2 阶:
结果2: 1阶 + 1阶(最后一步前的位置) + 2阶
结果5: 2阶(最后一步前的位置) + 2阶
啧啧啧,那我们之前大概看出的公式,套用在这里看看成不成立:
总方法数 = 最后一步前在第 2 阶的方法数 + 最后一步前在第 3 阶的方法数
卧槽,是不是有种豁然开朗的感觉,果然成立了。
既然前面的推测成立了,那第 n 阶的话怎么套用呢?
最后一步踏上第 n 阶,那么前一步应该就是在:
第 n-1 阶 或者 第 n-2 阶
所以:
踏上第 n 阶 的方法数 = 踏上第 n-1 阶的方法数 + 踏上第 n-2 阶的方法数
由此得到公式:
f(n) = f(n-1) + f(n-2)
这么一看,这不就是个 肥boy纳妾(斐波那契) 的数列啊。
那么,我们对着道题的解法就是如下(Golang):
func climbStairs(n int) int {
res := int(1)
lres := int(0)
for i:=1;i<=n;i++ {
res, lres = res + lres, res
}
return res
}
题目二:解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
'A' -> 1
'B' -> 2
...
'Z' -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
- “AAJF” ,将消息分组为 (1 1 10 6)
- “KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "0"
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 'J' -> "10" 和 'T'-> "20" 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。
示例 4:
输入:s = "06"
输出:0
解释:"06" 不能映射到 "F" ,因为字符串含有前导 0("6" 和 "06" 在映射中并不等价)。
提示:
- 1 <= s.length <= 100
- s 只包含数字,并且可能包含前导零。
题目分析——思考路线:
这个题目,起始就是要把一个数字序列分割成一个个大于0小于27的数(因为字母有26个,所以不能超过27),求分割方法数。
看到这道题目是不是有种似曾相识的感觉?像不像爬楼梯题目的兄弟?
那这道题怎么解决呢?
首先我们需要先把目标数字序列的每个数字,按照以下规则进行换算:
(字母数字:大于0小于27的数字)
成为字母数字的条件:
A:当前数字大于 0
B:当前数字能和前一个数字组合成字母数字
规则:
A成立 B成立:换算为 2
A成立 B不成立:换算为 1
A不成立 B成立:换算为 1
A不成立 B不成立:换算为 0
举例,按照以上规则换算下面数字:
1 | 2 | 3 | 5 | 1 | 2 | 7 | 1 | 0 | 2 | 1 | 7 | 8 | 1 | 2 | 3 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 2 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 2 | 2 | 1 | 1 | 2 | 2 |
1 | 2 | 3 | 5 | 1 | 2 | 7 | 1 | 0 | 2 | 1 | 7 | 0 | 1 | 2 | 3 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 2 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 2 | 2 | 0 | 1 | 2 | 2 |
这个时候,可能小伙伴还不知道换算的数值有啥用,别着急,听我细细说与你听。
换算的值只有三个:0、1、2
那么它和分割方法数有什么关系呢?
举例:
1 | 2 | 1 | 6 | 7 | 1 | 8 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 2 | 2 | 1 | 1 | 2 | 0 | 1 | 1 | 1 | 2 | 1 |
这里我们将举例说明 换算值 k(n) 和 分割方法数f(n) 的关系:
当 k(n) = 0 时:
整个数列无法分割成一个所有元素都为字母数字的数列
如上面例子中 0,其分割情况为:80,0
两个都不能成为字母数字
所以 f(n) = 0
当 k(n) = 1 且 第 n 个数字不为 0 时:
当前数字要么:
只能独自成为字母数字
要么:
和前一个数字组合成为字母数字
如上面的例子中的 7 的分割方法有以下几种:
(1、2、1、6)、7
(1、2、16)、7
(1、21、6)、7
(12、1、6)、7
(12、16)、7
我们把1216分割的所有方法写成:(1216)
则 7 的分割方法写成如下:
(1216)、7
而(1216)= f(4)
我们发现一共5种,f(4) = 5 ,数字 7 在(1216)的每一种情况后面加了一个 7
并没有额外增加分割方法,所以:
f(5) = f(4) ✖️ 1
即 : f(n) = f(n-1)
当 k(n) = 1 且 第 n 个数字为0 时:
当 k(n-1) = 1 时:
如上面例子中的倒数第二个 0 的分割方法有以下几种:
(12167180)、10
而 (12167180)= f(8)
数字 0 在(121671801)的每一种情况最后的1变成了10
并没有额外增加分割方法,所以:
f(10) = f(8)✖️ 1
即 : f(n) = f(n-2)
当 k(n-1) = 2 时:
如上面例子中的最后一个数字 0 的分割方法有以下几种:
(12167180101)、10
而(12167180101)= f(11)
数字 0 被限制在了 第 12 个数字上,所以第 12 个数字不能和前面的数字绑定了
所以:
f(13) = f(11) ✖️ 1
即 :f(n) = f(n-2)
当 k(n) = 2 时:
当 k(n-1) = 1 时:
如上面例子中的 8 的分割方法有以下几种:
(12167)、1、8
(12167)、18
则 f(7) = f(5) ✖️ 2
即 :f(n) = f(n-2) ✖️ 2
当 k(n-1) = 2 时:
如上面例子中的 6 的分割方法有以下几种:
(121)、6
(12)、16
则 :f(4) = f(3) + f(2)
即 :f(n) = f(n-1) + f(n-2)
总上所述,计算分割方法数的公式为:
当 n = 1 时: f(n) = 1
当 n > 1 时:
——————————————————————————————
当 k(n) = 0 时: f(n) = 0
__________________________
当 k(n) = 1 时:
当 数字为 0 时:
当 k(n-1) = 1 时: f(n) = f(n-2)
————————————————————————————————
当 k(n-1) = 2 时: f(n) = f(n-2)
————————————————————————————————
当数字不为 0 时:
f(n) = f(n-1)
————————————————————————————————
当 k(n) = 2 时:
当 k(n-1) = 1 时:f(n) = f(n-2) ✖️ 2
————————————————————————————————————
当 k(n-1) = 2 时:f(n) = f(n-1) + f(n-2)
由此可的得到代码如下(Golang):
func numDecodings(s string) int {
if len(s) == 0 || s[0] == 48 {
return 0
}
// 初始化 f(n)
res := 1
// 初始化 k(n)
r := 1
// 记录 f(n-1)
lres := 1
ls := int32(0)
for _, ss := range s {
// ss 为byte格式 48-54 代表 数字0-6
if ss == 48 && (ls > 50 || ls < 49) {
return 0
}
if ss > 48 && (ls == 49 || (ls == 50 && ss >= 48 && ss <= 54)){
if r == 1 {
res, lres = res * 2, res
} else if r == 2 {
res, lres = lres+res, res
}
r = 2
} else {
if ss == 48 {
res, lres = lres, res
} else {
lres = res
}
r = 1
}
ls = ss
}
return res
}