前言
在本文,我们要与一只活泼可爱的小青蛙合作,这个小家伙精力充沛,特别擅长于跳跃。我们要让它做我们的思维助手,看看有多少种方法让它跳到指定的台阶上。
本文比较生动有趣,没有太多的理论,小青蛙也非常敬业,相信对你来说,阅读本文将是一个愉快的经历。
我还得温馨地提醒一下你:
本文易懂(不难),但还是值得琢磨的。有些思维方法乍一眼看起来很像,代码写出来似乎也差不多,但是它们之间的解题方法,确实有差别的,你可能需要仔细体会,才能领悟。
题目
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
例如:
输入
5
对应的输出为
8
分治法(Divide and Conquer Method)
分析
秦灭六国的故事,大家多少有点了解。
简单来说,秦对六国是逐个瓦解,各个击破的。秦王显然是使用了分治法。
分治法的精髓是:分开(Divide)再(and)逐个征服(Conquer)。如果六国紧密抱团抗秦,结果还不好说。
分治法的思想:
把大问题分成若干小问题,再逐一解决这些小问题,最后把结果一汇总,得到问题的解。
我们来看看如何使用分治法解决青蛙跳台阶的问题。
n级台阶的跳法是个大问题,如何进行分解。
它可以分解为(n>2时):
n-1级台阶的跳法,和n-2级台阶的跳法。
因为青蛙跳到n级台阶之前,青蛙只有两种方式跳过来,一是从第n-1级台阶跳过来,或是从第n-2级台阶调过来。
n-1级台阶的跳法(n-1>2时),又可分解为n-2级台阶的跳法,和n-3级台阶的跳法。
依此类推,问题规模不断缩小。如此递推下去,必然就得到问题的解。
解答
import unittest
solution_count = 0
# 分治法
def frog_leap_divide_conquer(steps: int, reset=False):
global solution_count
if reset:
solution_count = 0
if steps == 0 or steps == 1:
solution_count += 1
return
frog_leap_divide_conquer(steps - 1)
frog_leap_divide_conquer(steps - 2)
class Test(unittest.TestCase):
def test_case_divide_conquer(self):
global solution_count
frog_leap_divide_conquer(6, True)
self.assertTrue(solution_count == 13)
归纳法(Induction Method)
分析
妈妈带你去买葡萄,希望买到的葡萄是甘甜的,而不是酸涩的。
怎么确定葡萄是甜的?
妈妈很有经验,她先挑一串葡萄,从上面随便揪下一两颗葡萄尝一尝(需要先征得水果店老板的同意)。如果吃到的葡萄是甜的,她就判断整串葡萄是甜的,可以买;如果吃到的葡萄是酸的,她就判断整串葡萄是酸的,那就不买!
妈妈用的就是归纳法。
我们观察青蛙跳台阶的跳法规律:
1级台阶的跳法,只有1种。
2级台阶的跳法,只有2种。
3级台阶的跳法,有3种。
4级台阶的跳法,有5种,这就需要拿出笔来写一写了。
5级台阶的跳法,有8种,拿笔写一写都有点烧脑了,好在我们通过归纳发现了规律。
我们设n级台阶跳法为M(n),那么存在以下规律:
M(1) = 1
M(2) = 2
n>2时,有:
M(n) = M(n-1)+M(n-2)
发现了逐个规律之后,题就好解了。
解答
import unittest
solution_count = 0
# 归纳法
def frog_leap_induction(steps: int):
if steps == 1:
return 1
if steps == 2:
return 2
return frog_leap_induction(steps - 1) + frog_leap_induction(steps - 2)
class Test(unittest.TestCase):
def test_case_induction(self):
self.assertTrue(frog_leap_induction(7) == 21)
穷举法(Exhaustive Method)
妈妈要你到院子里,把柠檬树上最大的柠檬摘下来。
你会把柠檬树上所有的柠檬都检查一遍,从中找出最大的,摘下来。
你用的就是穷举法。
我们来使用穷举法解决青蛙台阶的问题:
- 从最开始(第0级台阶)起,青蛙每次跳台阶之前都要面临一个选择:我这次是跳1级还是跳2级?
- 把每种选择都尝试一遍,只要不做重复的选择即可。
- 当青蛙跳到第n级台阶时,就记录得到一种解法。所有的解法累加,就得到跳法的总数。
解答
import unittest
solution_count = 0
# 穷举法
def frog_leap_exhaustive(cur_step: int, steps: int, reset=False):
global solution_count
if reset:
solution_count = 0
if cur_step > steps:
return
if cur_step == steps:
solution_count += 1
for i in range(1, 3):
frog_leap_exhaustive(cur_step + i, steps)
class Test(unittest.TestCase):
def test_case_exhaustive(self):
frog_leap_exhaustive(0, 8, True)
global solution_count
self.assertTrue(solution_count == 34)
回溯法(Backtrack Method)
分析
你玩过象棋(五子棋也行)吧?当你走完一步棋后意识到这步棋很臭时,你就要悔棋(你的对手可能也会后悔跟你这个爱悔棋的家伙下棋)。悔棋用的就是回溯法。
回溯法特点就是,你是通过不断向前探索(试错)来解决问题。当探索到某一步时,发现这一步不行,就退回一步重新选择,这种走不通就退回再走的方法就是回溯法。
我们来使用回溯法解决青蛙台阶的问题:
- 从最开始(第0级台阶)起,青蛙每次跳台阶之前都要面临一个选择:我这次是跳1级还是跳2级?
- 把每种选择都尝试一遍,只要不做重复的选择即可。它先按一种选择跳到对应到的台阶,记下自己所在级数,深入探索。探索完一种选择,再跳回到选择之前的位置(从当前自己所在级数往回退选择所对应的级数,返回到之前的现场),尝试下一个选择。
- 当青蛙跳到第n级台阶时,就记录得到一种解法。所有的解法累加,就得到跳法的总数。
解答
import unittest
solution_count = 0
# 回溯法
def frog_leap_backtrack(cur_step: int, steps: int, reset=False):
global solution_count
if reset:
solution_count = 0
if cur_step == steps:
solution_count += 1
for i in range(1, 3):
if cur_step+i <= steps:
cur_step += i
frog_leap_backtrack(cur_step, steps)
cur_step -= i
class Test(unittest.TestCase):
def test_case_backtrack(self):
frog_leap_backtrack(0, 9, True)
global solution_count
self.assertTrue(solution_count == 55)
动态规划(Dynamic Programing Method)
分析
在生活中,我们很少使用动态规划。我们直接从“青蛙跳台阶”的问题讲动态规划,而且“青蛙跳台阶”问题本身也足够生动有趣,用它来讲动态规划,没毛病。
你灵感乍现,列出如下表格,企图找出规律来解题。
通过列表格找规律确实是一个有效的方法。
| 台阶级数 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 跳法数量 | 1 | 2 | 3 | ? | ? |
台阶级数为1、2、3时,你凭直觉就能得到解,因为很简单:
- 如果台阶级数为1,当然只有1种跳法。
- 如果台阶级数为2,那有2种跳法:”跳1级,跳1级“和”跳2级“。
- 如果台阶级数为3,那有3种跳法:”跳1级,跳1级,跳1级“、”跳1级,跳2级“和”跳2级,跳1级“。
但是如果台阶级数到了4,情况似乎就比较复杂,穷举各种跳法的话,到了5级台阶,大脑就不太好用了。
找规律,找规律。
你前面填的表和后面要填的表格有什么联系?
在达到第4级台阶前,这只青蛙在什么地方?有两种可能:它要么从第3级台阶跳过来,要么从第2级台阶跳过来,没有其他的可能。
也就是说:青蛙跳到第4级台阶的跳法=青蛙跳到第3级台阶的跳法+青蛙跳到第2级台阶的跳法
对第5、6、...、n级台阶,情况均是如此。
抽象一下,设f(n)为青蛙跳到第n级台阶的跳法,则有:
f(n) = f(n-1)+f(n-2) (n>2)
这个叫状态转移方程,表示问题当前阶段和之前阶段的关系。有了这个关系,当前阶段的解就变成之前阶段的叠加,因此复杂的问题可以得到解决。
如此,表格就能容易继续填下去了:
| 台阶级数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... |
|---|---|---|---|---|---|---|---|---|
| 跳法数量 | 1 | 2 | 3 | 5 | 8(3+5) | 13(5+8) | 21(8+13) | ... |
这就是动态规划的解题方法。
我就不摘抄复杂的理论了,但我觉得有必要对动态规划法,做一点总结:
要使用动态规划法解题,一定要找到状态转移方程,如果有的话。
这个状态转移涉及到一个函数,函数自然就涉及到变量。这个函数可能不止一个变量,如果涉及多个变量的话,情况就更加复杂。所以使用动态规划解题,很多时候具备一定的挑战性。
解答
import unittest
solution_count = 0
# 动态规划
def frog_leap_dp(steps: int):
solution_count_list = [1, 2]
if steps == 1 or steps == 2:
return solution_count_list[steps-1]
for i in range(2, steps):
solution_count_list.append(0)
solution_count_list[i] += solution_count_list[i-2]
solution_count_list[i] += solution_count_list[i-1]
return solution_count_list[steps-1]
class Test(unittest.TestCase):
def test_case_dp(self):
self.assertTrue(frog_leap_dp(10) == 89)