Codility刷题之旅 - Exercise4 - Algorithmic skills

495 阅读4分钟

Exercise4 - Algorithmic skills

FirstUnique: Find the first unique number in a given sequence.

image.png

问题概述:输入是一个包含了N个整数的Array A,需要的返回值,是其中按顺序的第一个unique number。unique number的定义,即Array A中仅出现过一次的元素。

解法概述:根据题意,因为N最大是10w,而每个N的取值最大可能10e,因此我们选择使用dictionary,将A中每个元素的值作为key,将出现过的次数记录为key对应的value,每出现一次就+1。最后再重新从头循环一次,并不断检查count_dict[a]==1时,符合条件则直接返回a,否则在全部循环结束后返回-1。

def solution(A):
    count_dict = {}
    for a in A:
        count_dict[a] = count_dict.get(a,0)+1
    for a in A:
        if count_dict[a]==1:
            return a 
    return -1

image.png

StrSymmetryPoint: Find a symmetry point of a string, if any.

image.png

问题概述:本题输入是一个字符串S,需要返回的值,是是否存在一个index,index左侧的子字符串,正好是index右侧的子字符串的镜像。并且空字符串的reverse还是空字符串,也就是单字符的字符串应该返回index=0。而如果S中不存在符合条件的index,则返回-1。

解法概述:结合题意不难推出,如果字符串的字符数是偶数,则不可能找到符合条件的index,因为任意index下左侧和右侧的字符个数都不一致,直接返回-1即可。而字符串个数是奇数时,则可以用双指针的思路,从S的首尾不断判断是否字符相等,然后不断向中间靠拢直到两指针相遇。期间如果发现字符不等则直接返回-1,反之如果成功相遇,则begin=end=需要返回的值。

def solution(S): 
    result = 0 
    begin, end = 0, len(S)-1 
    if end%2==1: # len(S)是偶数 
        return -1 
    while begin<end: 
        if S[begin]==S[end]: 
            result += 1 
            begin += 1 
            end -= 1 
        else: 
            return -1 
    return result

image.png

TreeHeight: Compute the height of a binary tree.

image.png image.png

问题概述:输入是一个Tree实例,需要返回的,是Tree的最长深度。Tree的每个node都有三个属性,x,l和r。其中x是node的值,l是左子树,r是右子树。

解法概述:因为最大深度是500,直接简单按递归思路进行最大深度的计算即可,具体如下:

from extratypes import Tree  # library with types used in the task

def solution(T):
    if T:
        if not T.l and not T.r:
            return 0
        return max(solution(T.l), solution(T.r)) + 1
    return 0

image.png

ArrayInversionCount: Compute number of inversion in an array.

image.png

问题概述:给定一个由整数组成的数组A,数组A的逆序对定义为满足i<j且A[i]>A[j]的(i, j)对数。计算数组A的逆序对数。

解法概述:最开始想到的解题思路是维护一个cnt_dict记录每种数值出现过的次数,然后从左到右循环数组A,每一步判断cnt_dict中符合逆序对的value个数有多少,加进总result中。并且这个过程中会不断在cnt_dict中减掉循环中上一个value的出现个数。

def solution(A):
    v_cnt_dict = {}
    for a in A:
        v_cnt_dict[a] = v_cnt_dict.get(a, 0) + 1
    # 从头统计一遍
    result = 0
    for a in A:
        v_cnt_dict[a] -= 1 # 当前a从v_cnt_dict中减掉
        for v, cnt in v_cnt_dict.items():
            if v<a:
                result += cnt
                if result > 1000000000:
                    return -1
    return result 

image.png

最终发现这个双循环思路,并非performance最优的解法,虽然Correctness达到了100%,但是Performance只有惨淡的20%。

借鉴了网上的解题思路,最优解法是用Merge Sort或树状数组来解决该问题,时间复杂度可以下降到O(NlogN)。

我这里后来选择利用的是归并排序思想的解法:将数组A不断拆分为较小的两个数组,计算其逆序对数目并同时排序,最后将两个排好序的数组合并为一个排好序的数组,将其逆序对数目加入总计数器中。借鉴的比较清晰的github上这个项目的代码:github.com/OSerHuang/C…

def merge(nums1, count1, nums2, count2):
    if count1==-1 or count2==-1:
        return [], -1
    merged_nums = []
    count = count1 + count2
    i = j = 0
    while i<len(nums1) or j<len(nums2):
        if j==len(nums2) or (i<len(nums1) and nums1[i]<=nums2[j]):
            merged_nums.append(nums1[i])
            i += 1
        else:
            merged_nums.append(nums2[j])
            j += 1
            count += len(nums1) - i
            if count > 1000000000: return [], -1
    return merged_nums, count
    
def merge_sort(nums):
    if len(nums) <= 1:
        return nums, 0
    return merge(*merge_sort(nums[:len(nums) // 2]), *merge_sort(nums[len(nums) // 2:]))

def solution(A):
    # write your code in Python 3.6
    return merge_sort(A)[-1]

image.png

DisappearingPairs: Reduce a string containing instances of the letters "A", "B" and "C" via the following rule: remove one occurrence of "AA", "BB" or "CC".

image.png

问题概述:输入是一个只包含小写英文字母的字符串S,迭代地将字符串中相邻且重复的字符对(例如aa)删除,直到不能再删除为止。需求的输出,是经过上述操作后最终的结果字符串。

解法概述:本身这道题应该是考核可以利用栈的结构实现不断的匹配消除判断,然后在最后顺序出栈,获得需要返回的结果字符串,但是我们在solution中,实现上没有利用栈的结构,而是在循环中随时维护pre_s(循环中倒数第二个字符)和afterS(最终的返回值)两个字符串变量。

pre_s==s可以判断确定是否需要删除重复字符,如果确定需要删除重复字符,那么就再用pre_s=afterS[-1]更新下pre_s的值即可。而为了避免开头两个字符就是重复字符,无法实现pre_s=afterS[-1]的操作,这里把初始化的afterS就设定为了"X",并在最终返回时跳过这个打头的X。

def solution(S):
    # 初始化一个非ABC字符,并在最后返回afterS时从第二位开始返回
    # 主要防止removing过程中afterS变成空字符后afterS[-1]部分报错
    afterS = 'X' 
    pre_s = ''
    for s in S:
        if s==pre_s: # 2个相同字符,销掉
            afterS = afterS[:-1]
            pre_s = afterS[-1]
        else:
            afterS += s
            pre_s = s
    return afterS[1:]

image.png

PolygonConcavityIndex: Check whether a given polygon in a 2D plane is convex; if not, return the index of a vertex that doesn't belong to the convex hull.

image.png image.png image.png

问题概述

解法概述:这个部分涉及了几个Step获取到最终的解:

  1. 凸多边形的判断
  2. 多边形方向确定规则:确定非凸多边形后,对输入的Array中点是沿多边形的顺时针还是逆时针的点进行判断
  3. 多边形凹点确定规则:确定了点的多边形方向后,可以顺序计算点组成的前后向量的叉乘,确定每个点是否为凹点,确定即可立刻返回当时的对应index

具体的算法,其实也是参考了网上的一些论文,在上边的三个Step中,每一部分其实都有不同的一些算法,我这里给出的solution只是选取了我比较好理解的一种。

【多边形方向的确定】参考了这一篇:www.shcas.net/jsjyup/pdf/…

【多边形凹点的确定】参考了这一篇:blog.csdn.net/u014622197/…

from extratypes import Point2D  # library with types used in the task

def solution(A):
    # Step1: 判断给定的多边形是否为凸多边形
    # 可以通过计算每个连续的三个点所组成的向量的叉乘来判断,若所有的向量叉积的符号都一致,则该多边形为凸多边形。
    result=0
    is_convex=True  
    for p0, p1, p2 in zip(A, A[1:]+A[:1], A[2:]+A[:2]):
        p0p1_x, p0p1_y = p1.x-p0.x, p1.y-p0.y
        p0p2_x, p0p2_y = p2.x-p0.x, p2.y-p0.y
        p0p1_p0p2 = p0p1_x * p0p2_y - p0p1_y * p0p2_x

        if p0p1_p0p2<0:
            if result>0:
                is_convex=False
                break 
            result -= 1
        if p0p1_p0p2>0:
            if result<0:
                is_convex=False
                break 
            result += 1

    # 多边形是凸多边形,返回-1;不是凸多边形,则它包含至少一个凹点
    if is_convex:
        return -1

    # Step2: 判断输入Array中的点顺序,是沿多边形顺时针还是逆时针的。
    # 具体算法,是找到所有点里y最小(y相同最小,则再取x最小)的点,因为可以证明这个点两侧的边组成的一定是凸点,所以可根据这个点左右两个边的向量叉乘,确定哪个点是顺时针的下一个点,
    min_point_i = 0
    min_x, min_y = A[0].x, A[0].y 
    for i,a in enumerate(A[1:]):
        if a.y<min_y:
            min_x, min_y = a.x, a.y
            min_point_i = i+1
        elif a.y==min_y:
            if a.x<min_x:
                min_x = a.x
                min_point_i = i+1
    is_clockwise = True # 默认初始化为顺时针
    tmp_A = [A[-1]] + A + [A[0]]
    p0, p1, p2 = tmp_A[min_point_i], tmp_A[min_point_i+1], tmp_A[min_point_i+2]

    p0p1_x, p0p1_y = p1.x-p0.x, p1.y-p0.y
    p1p2_x, p1p2_y = p2.x-p1.x, p2.y-p1.y
    p0p1_p0p2 = p0p1_x * p1p2_y - p0p1_y * p1p2_x

    if p0p1_p0p2>0: # P0~P1是沿着多边形逆时针走的,反之则是顺时针,不需要修改is_clockwise
        is_clockwise = False

    # Step3: 找到凹点    
    # 在已确定点是按多边形的顺时针还是逆时针后,就可以使用以下算法来找到凹点了:
        # 1. 从多边形上任选一个点P作为起点,顺序走到下一个点Q,假设PQ组成边A
        # 2. 若定义从Q继续走向下一个点R得到的边为B,则计算A、B的叉乘,
        # 3. 顺时针走的话,则每次两个边的叉乘为负,代表凸点;叉乘为负,代表凹点;逆时针走完,则判断条件相反
    for i, (p0, p1, p2) in enumerate(zip(A[-1:]+A[:-1], A, A[1:]+A[:1])):
        p0p1_x, p0p1_y = p1.x-p0.x, p1.y-p0.y
        p1p2_x, p1p2_y = p2.x-p1.x, p2.y-p1.y
        p0p1_p0p2 = p0p1_x * p1p2_y - p0p1_y * p1p2_x

        if is_clockwise and p0p1_p0p2>0:
            return i
        if not is_clockwise and p0p1_p0p2<0:
            return i

image.png