图解LeetCode系列(1)--爬楼梯算法的几种方案(含视频讲解)

前言

很多同学在平时的工作中主要是完成业务的开发,这固然没有问题,但如果局限于此的话,路子就会比较窄,相反如果能在其他更高级的技术上有所突破的话,路就会越走越宽。不知道大家有没有听过/看过这样一句话,数据结构与算法才是程序的灵魂。所以,从今天开始,我会不定期分享一些算法学习(刷题)的经验,希望对那些致力于成为高级/资深工程师对同学有所帮助。这是一个新的篇章,也是进阶之路迈出的重要一步。

本文也同步做了视频,大家可以关注一下
https://www.bilibili.com/video/BV1jK4y187g2/

题目

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。
难度:简单

这道题在LeetCode上算是比较经典的,可以说是算法入门的必学题目之一,也是很多大厂面试的题目之一。下面就来分析一下:

台阶排列分析.png

这里有个很明显的规律,第n个台阶的走法=第(n-1)个台阶的走法 + 第(n-2)个台阶的走法。很明显符合斐波拉契数列规律:1、2、3、5、8、13、24......。所以就诞生了第一种解法

1、递归法

  /**
     * 每次最多可以跨2阶,利用了斐波那契数列规律增加,即f(n) = f(n-1) + f(n-2)
     *
     * @param n
     * @return
     */
    private fun calculate1(n: Int): Int {
        return if (n == 1 || n == 2) {
            n
        } else {
            calculate1(n - 1) + calculate1(n - 2)
        }
    }

这种方案在逻辑上的确简单,但是却有个致命的缺点,当台阶数稍微大一点的时候就算不出来了,甚至是发生爆栈StackOverflowError

2、循环法

循环法.png

我们来看4阶的走法,一共有5种走法,它的结算很简单,就是看成从3阶再走一步或者从2阶再走2步;而1、2、3阶的走法有多少种我们是知道的,所以直接循环就可以做。

    private fun calculate2(n: Int): Int {
        return if (n == 1 || n == 2) {
            n
        } else {
      // one保存倒数第二个子状态数据,two保存倒数第一个子状态数据, temp 保存当前状态的数据
            var one = 1
            var two = 2
            var temp = 0
            for (i in 2 until n) {
                temp = one + two
                one = two
                two = temp
            }
            temp
        }
    }

这里也是利用了f(n) = f(n-1) + f(n-2),不过是通过遍历的方式来实现的,这里要计算出第1~n-1项各自的走法数,然后倒数第二项+倒数第三项就是最终的结果。

3、动态规划法

什么是动态规划?
动态规划是求解决策过程最优化的数学方法。基本思想是:把当前问题拆分成多个子问题,再合并子问题的解就可以得出当前问题的解;此外,一旦某个给定子问题的解已经算出,会被存储起来,下次需要的时候直接获取。很明显这种解法比递归更高效,因为递归本质上就是遍历二叉树,做了大量的重复计算。

动态规划三要素:

  • 最优子结构
  • 边界
  • 状态转移公式
动态规划.png

按照动态规划的思想,爬n阶楼梯可以转化成:

  • 1、f(n) = f(n-1) + f(n-2):具体来说,n-1阶楼梯的走法再走1阶 + n-2阶楼梯的走法再加2阶 ;
  • 2、 f(n-1) 和 f(n-2)可以继续拆分,直到f(1)和f(2),由于f(1)和f(2)是已知的,所以可以由多个子解合并得到f(n)的解;
  • 3、为了保存子解的状态,可以用一个数组来存储爬第 i 阶楼梯的走法数。

三要素具体为:

  • 1、f(n-1),f(n-2)就是f(n)的最优子结构 ;
  • 2、f(1) = 1 ,f(2) = 2就是边界
  • 3、f(n) = f(n-1)+f(n-2)就是状态转移方程
   /**
     * 动态规划法 (利用数组来存储)
     */
    private fun fib04(n: Int): Int {
        if (n == 0) return 1
        val array = IntArray(n + 1)
        array[0] = 1
        array[1] = 1
        for (i in 2..n) {
            array[i] = array[i - 1] + array[i - 2]
        }
        return array[n]
    }

既然了解了动态规划的思想,那再回头看循环法的解决思路,其实它也是动态规划算法,这是说它用变量来存储的,而用数组的话在某些场景更加灵活。

扩展

爬N阶楼梯,每次最多可以爬M阶,M<=N,问有多少种走法?

递归法

    /**
     * @param n 要爬行n阶楼梯
     * @param maxJump 每次最大跨越数
     */
    private fun calculate4(n: Int, maxJump: Int): Int {
        var jumpCount = 0
        if (n == 0) {
            return 1
        }
        if (n >= maxJump) {
            // 剩下的楼梯大于最大可跳跃数
            for (i in 1..maxJump) {
                jumpCount += calculate4(n - i, maxJump)
            }
        } else {
            // 剩下的楼梯不足最大可跳跃数
            jumpCount = calculate4(n, n)
        }
        return jumpCount
    }

这里递归逻辑非常简单,不过还是讲一下吧。这里分2种情况讨论:
1、n >= maxJump,剩下的楼梯大于最大可跳跃数
因为需要知道前maxJump阶的走法,才能计算大于最大可跳跃数的情况的值。
2、n < maxJump,剩下的楼梯不足最大可跳跃数
直接递归就可以解决,只需要让maxJump=n

动态规划

分析:刚才2阶的时候,其实已经讲过这个思路了,总结出了结论,f(n) = f(n-1)+f(n-2)。现在变成每次最大M阶了,其实也一样的,不过要分2种情况讨论:
1、n>=m,可以看成,f(n-1)再走1阶 +f(n-2)再走2阶+ ... f(n-m)再走m阶,所以f(n,m)=f(n-1)+f(n-2)+...+f(n-m)
2、nf(n,m)=f(0)+f(1)+...+f(n-1)

f(m,n)通用公式.png
private fun fun4(n: Int, m: Int): Int {
        if (n == 1 || n == 2) {
            return n
        }
        val array = IntArray(n + 1)
        array[0] = 1
        array[1] = 1

        //分2种情况讨论
        //1.n=m.f(n,m)=f(n-1)+f(n-2)+..+f(n-m)
        for (i in 2..n) {
            val max = if (i < m) i else m
            for (j in 1..max) {
                array[i] += array[i - j]
            }
        }
        return array[n]
    }

有了上面的通用公式,上面的代码就很好理解了。重点讲一下2个for循环,第一个大的for循环是为了计算出f(2)~f(m),每阶各自的走法数;第二个大的for循环是为了计算出f(m)~f(n),每阶各自的走法数。

变形题

题目:
每件商品价格为1~9元(只有整数),要凑够满99满减的活动有多少种购买方案?

看上去是不是很像上面提到的 f(n,m)阶 楼梯问题。这里的n=99,m=9,那只需要调用上面的方法就可以完成,是不是很简单?这种解法确实没毛病,也很快,算是一种通用思维,如果面试的时候你能回答出这种方案,就算是过关了。这道题应该还有别的思路,大家可以思考一下

参考

https://blog.csdn.net/wu2304211/article/details/52717578
https://zhuanlan.zhihu.com/p/30754890
https://segmentfault.com/a/1190000015944750

你可能感兴趣的:(图解LeetCode系列(1)--爬楼梯算法的几种方案(含视频讲解))