剑指Offer跳台阶

Posted iwiniwin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指Offer跳台阶相关的知识,希望对你有一定的参考价值。

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

解法1

首先对这道题,我们可以通过找规律来解
一只青蛙可以跳上1级台阶,也可以跳上2两级台阶
当n = 1时,有1种跳法
当n = 2时,有2种跳法
当n = 3时,有3种跳法
当n = 4时,有5种跳法
当n = 5时,有8种跳法
...
等等,1,2,3,5,8...,多么熟悉的数列,斐波那契?
仔细想想当有n(n >= 2)级台阶时,求F(n)
青蛙第一步可以选择跳上1级台阶,则还剩n - 1级台阶需要跳,即F(n - 1)
青蛙第一步也可以选择跳上2级台阶,则还剩n - 2级台阶需要跳,即F(n - 2)
则总的跳法F(n) = F(n - 1) + F(n - 2),毫无疑问这就是斐波那契数列的定义了。
关于斐波那契的求解方法,读者可以参考【剑指Offer】斐波那契数列,包括了递归,动态规范,矩阵快速幂多种解法,这里就不再赘述了。
下面放上以求解斐波那契数列的方式解题的其中一种写法。

实现代码

public int jumpFloor(int number)
{
    int f1 = 0;
    int f2 = 1;
    while (number-- > 0)
    {
        f2 = f1 + f2;
        f1 = f2 - f1;
    }
    return f2;
}

排列组合

网上大多数的解法都是以求解斐波那契数列的思路来解题,但其实使用排列组合的思想也可以求解(真相是一开始没发现是斐波那契,才想到排列组合的-_-||)。
解题前先简单介绍下排列组合,摘自百度百科

  • 排列的定义:从n个不同元素中,任取m(m≤n,m与n均为自然数,下同)个元素按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列。从n个不同元素中取出m个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,用符号A(n,m)表示。
    \\[A{^m_n} = n * (n - 1) * (n-2) * ... * (n-m+1) = \\frac{n * (n - 1) * (n - 2) * (n - m + 1) * (n - m) * ... * 2 * 1}{(n - m) * (n - m - 1) * (n - m - 2) * ... * 2 * 1} \\]
    \\[A{^m_n} = \\frac{n!}{(n - m)!}\\]
    其中n!表示n的阶乘,比如4! = 4 * 3 * 2 * 1
    对于\\[A{^m_n} = n * (n - 1) * (n-2) * ... * (n-m+1)\\]可以这样理解,当从n个元素中取第一个元素时,有n种取法。当再取第2个元素时,此时还剩n - 1个元素,有n - 1种取法。当再取第3个元素时,此时还剩n - 2个元素,有n - 2种取法。以此类推,当取到第m个元素时,此时还剩n - m + 1个元素,有n - m + 1种取法。则总取法为\\[n * (n - 1) * (n-2) * ... * (n-m+1)\\]
  • 组合的定义:从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合。从n个不同元素中取出m个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号C(n,m) 表示。
    \\[C{^m_n} = \\frac{A{^m_n}}{m!} = \\frac{n!}{(n - m)!m!}\\]
    排列和组合的区别可以先简单理解为,从n个不同元素中,任取m个元素,有多少种取法,如果这m个元素的先后顺序不同,就认为是不同的取法的话,那么总取法数就是排列数A(n,m)。如果取出来的m个元素无论先后顺序如何,都认为是同一种取法,那么总取法数就是组合数C(n,m)。举个栗子,在{1,2,3,4,5}中任取三个数{2, 4, 5}或者{5,2,4},对于排列数来说,它们属于2种取法,而对于组合数来说它们只算1种取法。
    可以发现,排列和组合的区别在于取出来的这m个元素是否有顺序性。当有顺序性时,这m个元素有A(m,m)种排列顺序,所以排列数A(n,m)除以A(m,m),去除m个元素顺序性的影响,得到的就是组合数C(n,m),即
    \\[C{^m_n} =\\frac{A{^m_n}}{A{^m_m}} =\\frac{A{^m_n}}{m!} \\]

解法2

回到本题,青蛙可以跳上1级台阶,也可以跳上2级台阶,
对于n级台阶来说,它最多可以跳 n/2 次 2 级台阶,也就是说总的跳法数是跳0次2级台阶跳法数,1次2级台阶跳法数,2次2级台阶跳法数 ... n/2 次2级台阶跳法数的总和
现在问题就变成了求跳指定次数2级台阶的跳法数
假设有n级台阶,指定要跳m次2级台阶,还剩下n - 2m个1级台阶,则青蛙一共要跳n - 2m + m = n - m次才能跳完。即在n-m次跳跃中选择m次跳2级台阶,有多少种跳法数呢,C(n - m,m)种。(注意这里不是An - m,m),因为选出的m次2级台阶没有顺序性)
所以我们求出C(n - 1,1),C(n - 2,2),C(n - 3,3) .. C(n - n/2, n/2),然后将其相加,记得再加上1(选0个2级台阶,1种跳法),就是总的跳法数
实现代码时注意求组合数C(n,m)的算法,需要做一些优化,主要有两点
先化简公式
\\[C{^m_n} =\\frac{A{^m_n}}{m!} =\\frac{n * (n - 1) * (n-2) * ... * (n-m+1)}{m!} \\]
1,如果C(n,m)的结果用c#的int类型存储,则最大值为2^31^-1,如果按照公式依次计算分子和分母,可能分子或分母会超过2^31^-1,从而造成异常。所以优化为分子和分母的计算同步进行,当判断分子除以分母可以除整的时候,就先相除,避免累计数过大。后面的代码会有对应注释。
2,可以省略的计算就省略
比如求解C(5,4),其中的4 * 3 * 2,分子分母可以直接相约,本来分子分母的计算都需要连乘4次,现在只需要连乘1次
\\[C{^4_5} = \\frac{5 * 4 * 3 * 2}{4 * 3 * 2 * 1} \\]

实现代码

// 求解cnm
public int c(int n, int m)
{
    int count = m;
    int nf = 1, mf = 1;
    if (m >= n - m + 1)
    {
        // 对应优化2,只需要连乘n-m次
        count = n - m;
    }
    for (int i =0; i < count; i++)
    {
        nf *= (n - i);
        mf *= (i + 1);
        // 对应优化1,可以除整的时候就先除
        if (nf % mf == 0)
        {
            nf = nf / mf;
            mf = 1;
        }
    }
    return nf / mf;
}

public int jumpFloor(int number)
{
    int ret = 1;
    int count = number / 2;
    for (int i = 1; i <= count; i++)
    {
        ret += c(number - i, i);
    }
    return ret;
}

以上是关于剑指Offer跳台阶的主要内容,如果未能解决你的问题,请参考以下文章

剑指offer:变态跳台阶

剑指Offer 跳台阶

剑指offer 变态跳台阶

剑指offer 跳台阶

剑指offer:跳台阶

剑指OFFER变态跳台阶