Codility刷题之旅 - Prefix Sums

392 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

今天继续Codility的Lessons部分的第5个主题——Prefix Sums。

Codility - Lessons - Prefix Sums

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

image.png

PDF Reading Material

本部分的PDF还是只有两部分:

  • 正文:1.介绍prefix sums的概念,以及计算prefix sums的方法和复杂度,即从数组第一个元素循环到最后一个即可完成截至每个元素的prefix sum的计算,时间复杂度为O(n)。2.介绍与之相对的suffix sums的概念。3.最后,介绍两种sums的主要应用场景:即计算好了prefix sums/suffix sums数组后,回答原始数组中第X到第Y个元素的加合是多少,只需要使用计算好的prefix sums数组做一个相减运算即可->P[Y+1]-P[X]

  • Exercise:是一个采蘑菇的场景,在一个长度为n的数组里,每一位都是一个随机的自然数,代表这一点上蘑菇的数量。假设起始点在数组中的某个位置k,并且一共可以自由移动m步,问最多可以采多少蘑菇。使用上边正文介绍的先计算prefix sums,再计算出最终结果的思路带入到这个场景里即可解决问题,时间复杂度为O(m+n)

Tasks

  • Task1: PassingCars image.png

题目输入是Array A,每个点代表一辆车,值为0代表travel to the east,值为1代表travel to the west。需要返回的输出值,即在A中有多少个(P,Q)组合是满足P=0,Q=1的。并且P一定在Q的左侧。

我们可以循环A数组中每一个值为0的位置作为P,并计算符合条件的Q有多少,然后再加总起来。而对于某个P(假设index=i),对应符合条件的Q有多少,就可以借助事先计算好的prefix_sums数组,利用prefix_sums[-1]-prefix[i+1]实现。

image.png

def calc_prefix_sums(A):
    n = len(A)
    P = [0] * (n + 1)
    for k in range(1, n + 1):
        P[k] = P[k - 1] + A[k - 1]
    return P
    
def solution(A):
    prefix_sums = calc_prefix_sums(A)
    # 初始化pairs为0,p_last是计算好的prefix_sums中的最后一位
    pairs = 0
    p_last = prefix_sums[-1]
    # 循环A中元素,遇到0时(P车travelling to the east),计算符合Q=1(Q车travelling to the west)的点数量
    for i, a in enumerate(A):
        if a==0:
            # 符合Q=1(Q车travelling to the west)的点数量,就是prefix_sums[-1]-prefix_sums[i+1]
            pairs += (p_last - prefix_sums[i+1])
    # 根据题干条件,返回pairs或-1
    if pairs>1000000000:
        return -1
    return pairs 

image.png

  • Task2: CountDiv image.png

本题的输入是A,B,K三个整数,其中A<=B。需要返回的是在A~B间(包含A、B)的全部整数里,一共有几个可以被X整除。

这题和prefix_sums的关联其实我没太想清楚,按我的解题思路,这就是一个很简单的把A和B分别与K相除取整数和余数(k1X+b1=A,k2X+b2=B),然后再把两个k做差的事情,然后针对b1是否为0,会决定k2-k1后是否需要+1。

def solution(A, B, K):
    # for A and B, y = NX + b
    N_A, b_A = A//K, A%K 
    N_B, b_B = B//K, B%K 
    # A%K==0和!=0时,满足条件的X个数会有1的差异
    if b_A==0:
        return N_B - N_A + 1 
    else:
        return N_B - N_A 

image.png

  • Task3: GenomicRangeQuery image.png

本题输入是1个代表DNA的字符串S,长度为N。和2个长度都为M的非空数组P、Q。对于P、Q中每组同index的值pi、qi,需要计算DNA中pi~qi这个片段的minimal impact factor,也就就是片段中所有nucleotide的impact factor里的最小值,每个nucleotide的impact factor具体为:A=1,C=2,G=3,T=4。

最终需要的输出是一个Array,每个index下的值,就是S的P[index]~Q[index]这段DNA片段的minimal impact factor。

本题解题思路也是注释就可以写明白,首次构建一个代表每个nucleotide的impact_factor_dict,然后直接迭代P、Q中的每对元素确定具体片段(S[p:q]),接着按照impact factor从小到大顺序,依次确认片段中是否包含A/C/G/T字符,判断是包含时就可以直接跳出了:

def solution(S, P, Q): 
    imp_fac_dict = {'A':1, 'C':2, 'G':3, 'T':4} 
    # 迭代P、Q中元素,计算min_impact_factor,并存入要返回的Array(ans_arr)中 
    ans_arr = [0 for p in P] 
    for i, (p,q) in enumerate(zip(P,Q)): 
        for k in imp_fac_dict: 
            if k in S[p:q+1]: 
                ans_arr[i] = imp_fac_dict[k] 
                break 
    return ans_arr

image.png

  • Task4: MinAvgTwoSlice image.png

本题的输入为包含N个自然数的Array A,需要输出的是可以得到Minimal AvgSlice的起始点index。AvgSlice的算法,是确定了起始点之后,其之后的至少包含2个元素的全部Slice的几何均值。也就是说,比如N=7,起始点为index=0,那此时对应的所有AvgSlice,就包含以下6个Slice的几何均值:【index=0~1,0~2,0~3,0~4,0~5,0~6】,然后Minimal AvgSlice就是这6个Slice的几何均值里最小的数。

这题的解法我想了很久也没想出来,最后不得已参考了谷歌。我看懂的解题思路,似乎还是和prefix_sums没什么关系:

首先,确定MinAvgSlice一定是2个元素或3个元素的Slice,因为如果长于3个元素,就一定可以分为两个Slice A、B,而无论Avg(Slice A)是大于、等于、还是小于Avg(Slice B),Slice(A+B)一定大于等于min(Avg(Slice A),Avg(Slice B))。

然后就可以在A里从index=0~index=N-2循环一遍,分别计算length=2/3的Slice的几何均值。再用2个常数分别记录出现过的MinAvgSlice值,及对应的MinAvgSlice的index,最终循环结束返回MinAvgSlice的index即可:

def solution(A):
    # 使用A[:2],初始化一个min_v和min_v_idx
    min_v = sum(A[:2])/2
    min_v_idx = 0
    # 迭代数组A,从index=0~index=N-2
    N = len(A)
    for i in range(N-1):
        # MinAvgSlice只会是length=2/3的Slice,且index=N-2时,只存在length=2的Slice
        if i==N-2:
            ls = (2,)
        else:
            ls = (2,3)
        for l in ls:
            tmp_v = sum(A[i:i+l])/l
            if tmp_v<min_v:
                min_v = tmp_v
                min_v_idx = i
    return min_v_idx

image.png