开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情
今天继续Codility的Lessons部分的第10个主题——Prime and composite numbers
Codility - Lessons - Prime and composite numbers
还是按先总结红色部分PDF,然后再完成蓝色的Tasks部分的节奏来~
PDF Reading Material
- 前言: 介绍了质数和合数的概念
- 10.1-Counting divisors: 此部分介绍对一个整数求正因子的过程,是一个复杂度为O()的解法。对k来说,从1开始循环完小于等于的所有整数,如果发现循环到的整数n存在k%n==0,则n为divisor,同时如果n==则只记录为1个divisor,反之算2个。
- 10.2-Primality test: 基于10.1的解法,可以写出一个测试一个整数n是否为合数/质数,复杂度为O()的检验函数。
- 10.3-Exercises: 测试题是一共有n枚硬币正面朝上,然后一共有n个人会来翻硬币,对于第i(i=1~n)个人,他会翻这n个硬币里所有是i的整数倍的硬币一次,最后问所有人翻完一遍后,有多少个硬币变成背面朝上了。
- 此部分给了两种解法,第一种是生成一个长度为n的数组Coins,然后循环n次模拟这n个用户的翻硬币过程,每次翻第k个硬币的时候,就更改Coins[k]的目前取值来模拟翻面,最终循环结束后再循环一遍看有多少个背面。整体计算复杂度为O(nlogn)
- 第二种解法还是利用求正因子时的思想,对于一个正整数k,如果他不存在的divisor,那么他的所有divisor一定是成对的整数,也是一定会被翻偶数次。所以本题的反面硬币数,就是位置序号可拆解成整数平方()形式的硬币数量,也就是(1,4,9...)这些位置。这样就可以实现复杂度为O(logn)的解法了。
Tasks
- CountFactors
本题只是换了个叙述方式的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
- MinParameterRectangle
本题还是求divisor的题,只是最终需要返回的值从divisor的个数,换成返回最小的Rectangle周长(也就是成对的两个divisor之和的2倍)。并且这里数学推导可以知道对于全部满足0<i<=的正整数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
- Flags
本题输入是一个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
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
- Peaks
本题还是基于上题的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