Codility刷题之旅 - Fibonacci numbers

394 阅读5分钟

今天继续Codility的Lessons部分的第13个主题——Fibonacci numbers

Codility - Lessons - Fibonacci numbers

还是按先总结红色部分PDF,然后再完成蓝色的Tasks部分的节奏来~

image.png

PDF Reading Material

  • 前言: 介绍什么是斐波那契数列(这个部分我就不解释了,大部分人应该都知道了,不知道就在网上搜索下吧)。 然后PDF介绍了两种计算斐波那契数列的第n个元素值的方式:
    方式1用的是递归,按照fib(n)=fib(n-1)+fib(n-2)进行计算。然而这种方式是很低效的,不管是时间复杂度,还是由于递归对内存带来的压力增长都是指数级增长的。
    方式2用的是动归,也就是从f(1)计算到f(n)的过程中,都直接基于已经计算好的前2个斐波那契值f(n-1),f(n-2)来得到f(n),而非像上边的递归方法一样,进行两次计算。这种算法的时间复杂度是O(n)的
    具体的代码实现方式可以参考PDF~

  • 13.1: 又介绍了两种可以让计算复杂度更低的斐波那契算法,一种是利用矩阵计算可以将时间复杂度降低到O(logn),另一种是直接通过一个数学公式可以直接计算出F(n)

  • 13.2: Exercise的问题,是有n个在1~m之间的整数,需要回答其中有多少个整数是可以被拆解成两个斐波那契额数列数之和的。
    解题思路是因为小于m的斐波那契数一共也没有多少,所以我们可以把1~m之间的2个斐波那契数之和的值全算出来,并标记在一个长度为m的array中的对应index处(或者我觉得直接用一个字典来维护就可以了),这样对于n个整数,每个整数的判断只需要常数级别的复杂度,该解法下,最终整体的复杂度是O(n+m)。

Tasks

FibFrog: Count the minimum number of jumps required for a frog to get to the other side of a river.

image.png image.png

题目概括:依然是很之前几章提到的青蛙过河问题,输入是一个Array A,其中的1代表叶子,0代表没有叶子,最终要返回青蛙过河需要的最少跳跃次数。只是这次青蛙每次可以跳跃的距离,变成了斐波那契数列中的任意一个数字均可。

解题思路:首先我们一次性计算出小于河宽[=len(A)]的所有Fibonacci numbers,然后利用BFS的思想,从左岸进行每一步所有可能到达的positions的广度优先搜索,具体每一步的搜索就是jumps_from函数部分实现的,然后paths就是持续维护的一个每一步的所有可能到达的positions,中间任何时候如果搜寻到可以到达彼岸的数字,就直接返回当时的steps+1,反之则继续搜寻。

def fibonacci(N):
    arr = [1, 1]
    while arr[-1] < N:
        arr.append(arr[-1] + arr[-2])
    return dict.fromkeys(arr[1:], 1)

def jumps_from(position, fb, A): 
    paths = set([])
    for i in fb: 
        newPos = position + i
        if newPos == len(A):
            return set([-1])
        elif newPos < len(A):
            if A[newPos] == 1: 
                paths.add(newPos)
        else: 
            break
    return paths
            
def solution(A):
    if len(A) < 3: 
        return 1     
    fibonaccis = fibonacci(len(A)) # 计算<=len(A)+1的全部斐波那契数,最后一个数可能>len(A)+1
    if len(A) + 1 in fibonaccis: # 可以1步到达彼岸
        return 1
    
    # BFS思想,计算jump的步数
    paths = set([-1])
    steps = 0
    while True:
        paths = set([idx for pos in paths 
                         for idx in jumps_from(pos, fibonaccis, A)])
        if len(paths) == 0: # 没有可以跳到的有叶子的地方了
            return -1
        if -1 in paths: # jumps_from里设置newPos==len(A)时返回-1,因此-1就是到达彼岸了
            return steps + 1
        steps += 1 # 也没有到达彼岸,继续下一步的Search
    return steps 

image.png

Ladder: Count the number of different ways of climbing to the top of a ladder.

image.png

def solution(A, B):
    max_A = max(A)
    if max_A==1:
        return A

    n, a_res = 2, [1,2]
    while n<max_A:
        a_res.append(a_res[-2]+a_res[-1])
        n += 1

    res = [a_res[a-1]%(2**b) for a,b in zip(A,B)]
    return res 

image.png

题目概述:爬梯子问题,输入是两个Array A和B,A、B等长都含有n个正整数,A、B中同位置的正整数a和b,a代表了梯子的高度,b代表了2的b次方。 最终需要返回的result,是跟A、B等长的一个Array,Array中每个index处的结果,就是根据同index的a,b计算得到。需要根据a算出高度为a的梯子的爬法,与2的b次方取余。

解法概述: 首先我们解决计算高度为a的梯子一共有多少种爬法这部分解法复杂度的问题,可以看到Solution函数中,我们维护了一个a_res,初始化了高度为1、2两种梯子的爬法总数进去,而后续随着高度不断增加1,对应的解法数量就是【减1高度梯子的解法+减2高度梯子的解法】。

然后我们直接在a_res列表中取到爬法,再与2**b进行取余操作。这样的算法最终Codility分析得到的算法复杂度为O(L2)O(L^2),且Peformance无法达到最佳的100%。

这个部分的优化,就是在与2**b进行取余运算的时候,采用位运算的优化算法。具体为什么可以用位运算来简化替代取余操作,我找到了掘金上已有的一篇文章,大家可以看下里面的序号2的解释:juejin.cn/s/%E5%BF%AB…

首先,2的n次方在二进制下的表示,即1左移n次,然后右边都是0补充。

然后,我们求出的余数,最大不会超过2^n-1。2^n-1对应的2进制表示,就是2的n次方的二进制表示,将最左边的1变成0,再把后续的0都变成1。

不难理解,对于a%(2^b)的结果,我们如果在二进制下进算,其实就是把a的二进制表示,和2^n-1的二进制结果进行一个与操作(&)即可,因此我们对Solution中计算res时的逻辑进行了修改,变成直接用位运算实现。

def solution(A, B):
    ......(同上)
    # res = [a_res[a-1]%(2**b) for a,b in zip(A,B)]
    res = [a_res[a-1] & (1<<b)-1 for a,b in zip(A,B)]
    return res 

这样改造后,算法复杂度就下降到了O(L),最终Performance也达到了100%。

image.png