轻松理解5种思维方法:以青蛙跳台阶为例

192 阅读6分钟

前言

在本文,我们要与一只活泼可爱的小青蛙合作,这个小家伙精力充沛,特别擅长于跳跃。我们要让它做我们的思维助手,看看有多少种方法让它跳到指定的台阶上。

本文比较生动有趣,没有太多的理论,小青蛙也非常敬业,相信对你来说,阅读本文将是一个愉快的经历。

我还得温馨地提醒一下你:

本文易懂(不难),但还是值得琢磨的。有些思维方法乍一眼看起来很像,代码写出来似乎也差不多,但是它们之间的解题方法,确实有差别的,你可能需要仔细体会,才能领悟。

题目

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

例如:

输入

5

对应的输出为

8

分治法(Divide and Conquer Method)

分析

秦灭六国的故事,大家多少有点了解。

简单来说,秦对六国是逐个瓦解,各个击破的。秦王显然是使用了分治法。

分治法的精髓是:分开(Divide)再(and)逐个征服(Conquer)。如果六国紧密抱团抗秦,结果还不好说。

查看源图像

分治法的思想:

把大问题分成若干小问题,再逐一解决这些小问题,最后把结果一汇总,得到问题的解。

See the source image

我们来看看如何使用分治法解决青蛙跳台阶的问题。

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)

分析

妈妈带你去买葡萄,希望买到的葡萄是甘甜的,而不是酸涩的。

怎么确定葡萄是甜的?

妈妈很有经验,她先挑一串葡萄,从上面随便揪下一两颗葡萄尝一尝(需要先征得水果店老板的同意)。如果吃到的葡萄是甜的,她就判断整串葡萄是甜的,可以买;如果吃到的葡萄是酸的,她就判断整串葡萄是酸的,那就不买!

fruit_shop

妈妈用的就是归纳法。

我们观察青蛙跳台阶的跳法规律:

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)

妈妈要你到院子里,把柠檬树上最大的柠檬摘下来。

你会把柠檬树上所有的柠檬都检查一遍,从中找出最大的,摘下来。

你用的就是穷举法。

lemon_tree2

我们来使用穷举法解决青蛙台阶的问题:

  • 从最开始(第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)

分析

你玩过象棋(五子棋也行)吧?当你走完一步棋后意识到这步棋很臭时,你就要悔棋(你的对手可能也会后悔跟你这个爱悔棋的家伙下棋)。悔棋用的就是回溯法。

xiaqi_small

回溯法特点就是,你是通过不断向前探索(试错)来解决问题。当探索到某一步时,发现这一步不行,就退回一步重新选择,这种走不通就退回再走的方法就是回溯法。

我们来使用回溯法解决青蛙台阶的问题:

  • 从最开始(第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)

分析

在生活中,我们很少使用动态规划。我们直接从“青蛙跳台阶”的问题讲动态规划,而且“青蛙跳台阶”问题本身也足够生动有趣,用它来讲动态规划,没毛病。

你灵感乍现,列出如下表格,企图找出规律来解题。

通过列表格找规律确实是一个有效的方法。

台阶级数12345
跳法数量123

台阶级数为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)

这个叫状态转移方程,表示问题当前阶段和之前阶段的关系。有了这个关系,当前阶段的解就变成之前阶段的叠加,因此复杂的问题可以得到解决。

如此,表格就能容易继续填下去了:

台阶级数1234567...
跳法数量12358(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)
​