Codility刷题之旅 - Counting Elements

386 阅读7分钟

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

今天继续Codility的Lessons部分的第4个主题——Counting Elements。

Codility - Lessons - Counting Elements

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

image.png

PDF Reading Material

这部分的PDF结构很简单,一共只有前言和Exercise两部分:

  • 前言:介绍了除了正常将数字作为元素保存在list中之外,如果我们的全部数字都是正整数且最大范围在已知的m个枚举值以内,可以用一个count_list进行数字的统计。具体来说,就是将index视为元素值,index下的具体value作为这个元素值出现的个数。

image.png

  • Exercise:练习题是有两个Array,A和B。我们的最终目的是交换A,B中各自一个元素,最终实现sum(A)=sum(B),PDF中顺序介绍了复杂度为O(n2n^2)、和复杂度为O(mnm*n)的两种解法

感兴趣的朋友可以去看下原文~

Tasks

Task1: FrogRiverOne

image.png

题干比较长,概括一下大概是输入是一个自然数X和一个Array A,自然数X代表河的宽度,Array A的每个index和对应值,代表index秒时一片叶子会落在离河这边v值远的地方。 需要的输出值,就是到多少秒时从,河这边到对面已经全部都铺满了叶子(也就是距离河这边1、2、...X远的地方都已经落过落叶了)。题目重点关注的还是Solution函数的Efficiency。

这个题目比较简单,解题思路可以直接在Solution里注释写明白,如下:

def solution(X, A):
    # River模拟的河水,0代表没有叶子,1代表有叶子
    River = [0] * X
    # N是没叶子的地方被首次填充叶子的次数,我们可以预先知道这个操作进行X次后,河道应该就被叶子填满了
    N = X 
    # 迭代Array A,即叶子落下的信息流
    for sec, pos in enumerate(A):
        # 如果叶子要落的地方(pos)还没有叶子(River[pos-1]==0),就给N减1
        if River[pos-1] == 0:
            N -= 1
            # 若N此次减1后为0,直接跳出并返回当时秒数(sec)
            if N==0:
                return sec
            # N减完部位0,将River[pos-1]加1,确保下次再在此处落叶时,不会再减N了
            River[pos-1] += 1
        else:
            continue

image.png

image.png

结果出来傻眼了,发现Correctness检查错了3个,看了下报错信息和题干,发现漏看了一句话:If the frog is never able to jump to the other side of the river, the function should return −1.

原来叶子是有可能最后落完都铺不满河水的,这个时候需要返回-1。于是在上边代码末尾、for循环外边加了个return -1,这回终于100%了。审题还是不能想当然。

image.png

Task2: PermCheck

image.png

本题输入为一个非空数组A,其中一共有N个自然数。需要的输出是一个判断结果,即这N个元素是否构成了一个permutation(排列),即包含了从1~N每一个元素,并且每个元素只出现过一次。若是则返回1,不是返回0.

本题和上一题类似,但也存在以下两点小差异:

  1. 本题如果某个自然数出现了第二次,那就已经可以提前判断不满足permutation并返回0
  2. 本题可能存在A中的自然数大于N的情况,这种也可以直接提前判断不满足permutation并返回0

因此最终解题函数如下:

def solution(A):
    N = len(A)
    # 构造记录1~N是否出现的seq_arr,和记录记录次数的seq_cnt
    seq_arr, seq_cnt = [0]*N, N
    # 迭代A中每个自然数
    for a in A:
        # permutation定义是1~N的连续,所以出现大于N的元素,直接返回0
        if a>N:
            return 0
        # 因为这次如果某个数字有重复,就一定不满足permutation了,直接返回0
        if seq_arr[a-1]>0:
            return 0 
        seq_cnt -= 1
        seq_arr[a-1] = 1
        if seq_cnt==0:
            return 1 
    return 0

image.png

Task3: MaxCounters

image.png

本题输入是一个自然数N、和一个包含M个自然数的Array A: 其中的N代表了N个计数器,Array A中的M个自然数,则代表了对这N个计数器的M轮操作,每轮的操作要看对应自然数具体的值决定:1)如果元素值在1~N之间,操作就是对第N个计数器进行+1; 2)如果元素大于N,操作就是对所有N个计数器取Max,并全部赋值为这个Max值。

最终需求的输出是一个长度为N的Array,为这N个计数器在M轮操作后的具体值。

因为两种操作要么是对单个计数器加1,要么是对全部计数器统一赋值为max,并且这个判断全部是根据元素和固定值N的大小关系来决定,所以我一开始想到的就是用一个长度为N的List和一个单独维护的、代表N个Counter中最大值的max_c就可以了,具体如下:

def solution(N, A):
    # 构造一个维护所有counter值的counter_arr,和保存所有counter截至当时最大值的max_c
    counter_arr = [0 for i in range(N)]
    max_c = 0 
    # 迭代A中的元素,并根据和N的大小关系进行每轮操作
    for a in A:
        # a<=N,则对第a-1个Counter进行+1操作
        if a<=N:
            counter_arr[a-1] += 1
            # 判断+1的Counter是否是最大值Counter,是的话更新下max_c
            if counter_arr[a-1]>max_c:
                max_c = counter_arr[a-1]
        # a>N,直接用现在维护的max_c,重新生成counter_arr
        else:
            counter_arr = [max_c for i in range(N)]
    return counter_arr

结果发现,评估结果的Peformance是不符合要求的,只有40%(2/5)。

image.png

回顾了下,发现问题出在"counter_arr = [max_c for i in range(N)]"这句逻辑上,由于M、N取值都在1~100000,所以在M次的循环中执行这句 O(N)复杂度的操作,本身复杂度就已经不是O(M)了,而变成O(M)~O(MN)之间。

重新修改逻辑,将Max Counter Operator这轮操作改用常量last_max_lvl来维护,并不直接将该值刷到counter_arr的元素中,只是在函数返回前统一刷一次(类似惰性更新)。果然这样改动后,Performance上升到了100%。

def solution(N, A):
    counter_arr = [0 for i in range(N)] # 维护所有counter值的Array,对Max Counter Operation惰性更新
    last_max_lvl = 0 # 维护最后一次Max Counter Operation时的值
    curr_max_v = 0   # 维护迭代过程中实时的最大counter值

    # 迭代A中的元素,并根据和N的大小关系进行每轮操作
    for a in A:
        # a<=N,则对第a-1个Counter进行+1操作
        if a<=N:
            if counter_arr[a-1]<last_max_lvl:
                counter_arr[a-1] = last_max_lvl+1
            else:
                counter_arr[a-1] += 1
            # 判断+1的Counter是否是最大值Counter,是的话更新下max_c
            if counter_arr[a-1]>curr_max_v:
                curr_max_v = counter_arr[a-1]
        # a>N,更新last_max_lvl为curr_max_v
        else:
            last_max_lvl = curr_max_v
    # 返回前,要把counter_arr里目前还小雨last_max_lvl的元素都修改为last_max_lvl
    return [v if v>=last_max_lvl else last_max_lvl for v in counter_arr]

image.png

Task4: Missing Integer

image.png

本题输入A是一个长度为N的Array,其中每个元素是在[-1000000, 1000000]中的整数,需要的输出,是在A中没有出现过的、最小的正整数。也就是从1开始的自然数,哪个没在A里出现过就返回哪个。

个人觉得相比第三题,本题比较简单。且解题思路基本和前边题目1、题目2类似,仍然是构造一个长度等于Array A的list来记录1~N每个自然数的有无,过程中注意特别处理a<0和a>N两种特殊情况,最后对exist_arr循环结束后兜底返回N+1。最终直接运行就获得了100%的Score。

def solution(A):
    # 构造一个长度为len(A)的array exist_arr,来保存1~len(A)每个自然数的出现与否
    N = len(A) 
    exist_arr = [0] * N 
    # 迭代A中元素,对>0的自然数,在exist_arr的对应index处置1
    for a in A:
        if 0<a<=N:
            exist_arr[a-1]=1
    # 迭代exist_arr,返回第一个为0处的index+1,如果都exist,返回len(A)+1
    for i,v in enumerate(exist_arr):
        if v==0:
            return i+1 
    return N+1

image.png