Codility刷题之旅 - Sieve of Eratosthenes

331 阅读2分钟

今天继续Codility的Lessons部分的第11个主题——Sieve of Eratosthenes

Codility - Lessons - Sieve of Eratosthenes

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

image.png

PDF Reading Material

  • 前言: 介绍了质数筛选算法(埃拉托斯特尼筛法),及对应的计算时间复杂度O(nloglogn)。这里我找到了一版中文的相关算法介绍供参考。
  • 11.1. Factorization: 介绍了轻微修改埃拉托斯特尼筛法的计算过程后,实现快速因子分解的算法。

Tasks

  • CountNonDivisible

image.png

本题输入是一个Array A,其中会有重复元素。然后需要的返回是一个和A相同长度的Array,返回Array中每个index处的值,是对应index的A中元素,不是A中多少个元素的因子。举个例子的话,就是对于只包含了2,3,6三个元素的Array A,因为“2不能被3和6整除,3不能被2和6整除,6能被2和3整除”,所以最终的返回就是[2,2,0]

本题我的初始解题思路还是没有直接想到本篇的因式分解,而是将A中元素进行排序后的一个双循环,最后根据每个元素在双循环时算好的结果,循环A一遍生成result Array。果然提交之后的Performance并不能达到题目的要求。

def solution(A):
    sort_A = sorted(A)
    N = len(A)  
    # 每个位置默认的non-divisor数量为N-1(不算自己)
    cnt_lst = [N-1 for a in A]
    cnt_dict = {}
    # 从左向右循环
    for i in range(N):
        a1 = sort_A[i]
        # 循环中,每个元素都和后续元素比较下是否可整除,可以的话后续元素的non-divisor数量-1
        for j in range(i+1, N): 
            b1 = sort_A[j]
            if b1%a1 == 0:
                cnt_lst[j] -= 1
        # 每次子循环后,记录下对应value的non_divisor数量到字典里,方便后续生成result array
        cnt_dict[a1] = cnt_lst[i]
    return [ cnt_dict[a] for a in A ]

image.png

这里最后转换了下思路,利用PDF中给的因式分解的解法实现了每个a的divisor list的计算,复杂度从之前的O(n2n^2)下降到了O(nlog(n)n*log(n)):

def solution(A):
    # 统计A中每个a出现次数的dict
    a_count = {}
    for a in A:
        a_count[a] = a_count.get(a,0)+1

    # 记录A中每个a的divisor list的dict
    a_divisors = {a: set([1,a]) for a in A}
    max_A = max(A)
    n = 2 
    while n**2<=max_A:
        cand_a = n 
        while cand_a<=max_A:
            if cand_a in a_divisors:
                a_divisors[cand_a].add(n) 
                a_divisors[cand_a].add(cand_a//n)
            cand_a += n
        n+=1
    
    # 生成要返回的result Array
    result = [len(A)] * len(A)
    for i, a in enumerate(A):
        result[i] -= sum([a_count.get(divisor,0) for divisor in a_divisors[a]])
    return result 

image.png

  • CountSemiprimes

image.png

题目输入是N、P、Q,其中N是自然数,代表P、Q两个Array的长度,而P、Q两Array中的每组同index下的元素p、q代表了两个自然数,需要我们计算的是p~q之间的semiprime的个数。

本题实际上是在找质数基础上的一个变种,变成了找符合是两质数之积(semiprime)的元素。同时又在此基础上叠加了一个需要快速计算两自然数之间的符合条件元素的个数的思想。

解题思路上,前半部分的找两个质数之积的元素(semiprime)思路上和质数筛选算法(埃拉托斯特尼筛法)基本一致,只需要思考清楚判断条件。发现判断条件其实就是一个自然数除了(1*本身)这组因子之外的因子个数,对于不符合的自然数,他剩余的可行因子拆解个数一定是大于2组的,同时要特别注意下等于某个数的3次方的特殊场景。

后半部分的根据给出的p,q自然数,确定p~q之间符合条件的元素个数的解法,跟之前章节提到过的prefix_sums也是类似的,也是先从左到右构建一个prefix_sums的数组,然后再根据p、q进行O(1)复杂度的运算即可。详情见下方solution:

def solution(N, P, Q):    
    check_arr = [0] * N
    i = 2
    while i<=N:
        cand_i = i
        while cand_i<=N:
            if cand_i//i>1: # 存在1的因子时不计数
                check_arr[cand_i-1]+=2
                # 特别处理一下是某个整数的三次方的情况
                if cand_i==i**3:
                    check_arr[cand_i-1]+=1
            cand_i += i
        i+=1
    # 计数为0<n<=4的,为semiprime
    check_arr = [c>0 and c<=4 for c in check_arr]
    # 计算好截至每个index处的semiprime个数,第一位多填一个0,方便最后一步计算res   
    cumu_semiprime_cnt = 0
    cumu_semiprime_cnt_arr = [0]
    for i, check in enumerate(check_arr):
        cumu_semiprime_cnt += int(check)
        cumu_semiprime_cnt_arr.append(cumu_semiprime_cnt)
    # 根据(p,q)的值,利用prefix_sums的思想进行O(1)复杂度的减法计算
    res = [cumu_semiprime_cnt_arr[q]-cumu_semiprime_cnt_arr[p-1] for p,q in zip(P,Q)]
    return res 

image.png