Codility刷题之旅 - Prime and composite numbers

389 阅读5分钟

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

今天继续Codility的Lessons部分的第10个主题——Prime and composite numbers

Codility - Lessons - Prime and composite numbers

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

image.png

PDF Reading Material

  • 前言: 介绍了质数和合数的概念
  • 10.1-Counting divisors: 此部分介绍对一个整数求正因子的过程,是一个复杂度为O(n\sqrt{n})的解法。对k来说,从1开始循环完小于等于k\sqrt{k}的所有整数,如果发现循环到的整数n存在k%n==0,则n为divisor,同时如果n==k\sqrt{k}则只记录为1个divisor,反之算2个。
  • 10.2-Primality test: 基于10.1的解法,可以写出一个测试一个整数n是否为合数/质数,复杂度为O(n\sqrt{n})的检验函数。
  • 10.3-Exercises: 测试题是一共有n枚硬币正面朝上,然后一共有n个人会来翻硬币,对于第i(i=1~n)个人,他会翻这n个硬币里所有是i的整数倍的硬币一次,最后问所有人翻完一遍后,有多少个硬币变成背面朝上了。
    • 此部分给了两种解法,第一种是生成一个长度为n的数组Coins,然后循环n次模拟这n个用户的翻硬币过程,每次翻第k个硬币的时候,就更改Coins[k]的目前取值来模拟翻面,最终循环结束后再循环一遍看有多少个背面。整体计算复杂度为O(nlogn)
    • 第二种解法还是利用求正因子时的思想,对于一个正整数k,如果他不存在k\sqrt{k}的divisor,那么他的所有divisor一定是成对的整数,也是一定会被翻偶数次。所以本题的反面硬币数,就是位置序号可拆解成整数平方(k2k^2)形式的硬币数量,也就是(1,4,9...)这些位置。这样就可以实现复杂度为O(logn)的解法了。

Tasks

  • CountFactors

image.png

本题只是换了个叙述方式的PDF-10.1的原题,直接按照PDF中给出的解法即可完美达标:

def solution(N):
    fact_num = 0
    i = 1 
    while i*i<N:
        if N % i == 0:
            fact_num += 2 
        i+=1
    if i*i==N:
        fact_num+=1
    return fact_num 

image.png

  • MinParameterRectangle

image.png

本题还是求divisor的题,只是最终需要返回的值从divisor的个数,换成返回最小的Rectangle周长(也就是成对的两个divisor之和的2倍)。并且这里数学推导可以知道对于全部满足0<i<=k\sqrt{k}的正整数i,i越大则周长越小。因此复用上题解题框架进行简单修改:

def solution(N):
    i = 1
    min_peri = 2*(1+N)
    while i*i<N:
        if N%i==0:
            min_peri =  2*(i+N//i)
        i+=1
    if i*i==N:
        min_peri = 4*i
    return min_peri

image.png

  • Flags

image.png

image.png

本题输入是一个Array A,然后需要的是在A中找到peak,并返回最大可以插的旗子数。插旗必须插在peak处,并且有个奇怪的规则是如果一共准备插入k个旗,则每两个旗子之间的间距至少是k。不过比较好想到的是,在判断k个旗子能否插下的时候,是可以无脑从第一个peak开始插的(这个就不解释了)。所以最终的判断方式代码如下,首先计算出peaks数组,然后从1开始循环选择不同的k,然后进行逻辑上的判断能否插满旗子即可,应该还是比较好理解的:

def solution(A):
    # 新建一个是否为peaks的序列
    N = len(A)
    peaks = [False] * N
    for i, (a1,a2,a3 )in enumerate(zip(A[:-2], A[1:-1], A[2:])):
        if a2>a1 and a2>a3:
            peaks[i] = True 
    # 从1开始循环判断,是否可以满足插对应数量旗子
    for i in range(1, N):
        flag_num = i
        pos = 0
        while pos<N and flag_num>0:
            if peaks[pos]: # 是peak,插旗并向后跳i距离
                flag_num -= 1
                pos += i
            else: # 不是peak
                pos+=1 
        if flag_num>0: # 本轮旗子插不完,返回i-1
            return i-1
    # 事实上旗子数量不可能大于根号N,一定已经提前跳出
    return N-1 

image.png

Correctness虽然达到了100%,但是Performance的表现并不如意。上述解法里,确定peaks数组的复杂度是O(N),而从1~N循环判断旗子是否插得完的整体复杂度是O(NlogN)。最终还是看了参考答案,有了优化的思路,就是先用O(N)的复杂度,基于peaks数组计算出一个next_peaks的数组,用来辅助在后续的循环判断中,在每次循环内实现常数级别的复杂度。另外也放上我参考的答案

def solution(A):
    # 新建一个是否为peaks的序列
    N = len(A)
    peaks = [False] * N
    for i, (a1,a2,a3 )in enumerate(zip(A[:-2], A[1:-1], A[2:])):
        if a2>a1 and a2>a3:
            peaks[i] = True 
    # 根据peaks生成一个next_peak_pos数组,用于下一阶段辅助进行插旗判断
    next_peak_pos = [-1] * N
    for i in range(N-2, -1, -1): 
        if peaks[i]:
            next_peak_pos[i] = i 
        else:
            next_peak_pos[i] = next_peak_pos[i+1]
    # 从1开始循环判断,是否可以满足插对应数量旗子
    for i in range(1, N):
        flag_num = i
        pos = 0
        while pos<N and flag_num>0:
            pos = next_peak_pos[pos]
            if pos==-1:
                break 
            flag_num-=1
            pos+=i
        if flag_num>0: # 本轮旗子插不完,返回i-1
            return i-1
    # 事实上旗子数量不可能大于根号N,一定已经提前跳出
    return N-1 

image.png

  • Peaks

image.png

image.png

本题还是基于上题的Peaks概念的一道题,不过后半部分的要求不一样了,本题是在找到全部peaks之后,返回符合要求的最大block数量。符合要求的block数量,是指A中全部元素可以平分到这些block中,并且每个block中至少包含1个peaks。

这里不难理解block=1一定是符合条件的,同时最大可能符合条件的block数,就是peaks的数量。所以在获取到全部peaks的index数组后,从len(peaks)作为初始block数不断循环减小,在第一个满足题目要求的block数处直接返回。

而判断满足题目要求的方法确实值得深思,如下方函数,除了N % block_num==0的判断外,最关键的是其中判断“每个block里都至少包含一个peak”的方法:从第一个peak的index开始循环,并根据peak_i和block_len的相除取整的关系动态+1,最后根据q_block和block_num是否相等,就可以确认了。只要这样判断的计算复杂度是最低的,能够满足题目对Performance的要求。

def solution(A):
    # 新建一个记录所有peak的index的序列
    N = len(A)
    peaks_i = []
    for i, (a1,a2,a3 )in enumerate(zip(A[:-2], A[1:-1], A[2:])):
        if a2>a1 and a2>a3:
            peaks_i.append(i+1)
    # 最多可以分成的block_num,就是len(peaks)个,所以初始化block的数量为len(peaks)
    peak_num = len(peaks_i)
    block_num = peak_num
    while block_num>1:
        # 假如A正好可以分成block_num个,则每个block长度为block_len
        if N % block_num==0:
            block_len = N//block_num 
            # 用来确认每个block里(block_len的距离),至少包含一个peak
            q_block = 0
            for peak_i in peaks_i:
                if peak_i//block_len==q_block:
                    q_block += 1
            # q_block和block_num相等,则说明每个block至少包含了一个peak,可直接返回block_num
            if q_block==block_num:
                return block_num
        block_num -= 1
    return block_num 

image.png