优化 Codility 标志的性能 Python

Optimising Performance of Codility Flags Python

我写了下面的算法作为 Codility Flags 的解决方案。这通过了正确性检查,但是它在大多数性能检查中超时。

这个的复杂度应该是 O(m**2),其中 mA 中的峰数,nA 的长度。但是,while potentialK > maxFlags 循环应该只执行到找到满足条件的合适数量的标志为止。我不确定如何针对时间复杂度进一步优化它。

def solution(A):
    peaks = []
    distances = []
    if len(A) == 1: return 0
    for i in range(1, len(A) -1): 
        if A[i] > A[i-1] and A[i] > A[i+1]:
            if len(distances) == 0: 
                distances.append(i)
            else: 
                distances.append(i - peaks[-1]) 
            peaks.append(i)
    if len(peaks) == 0: return 0
    if len(peaks) == 1: return 1
    if len(peaks) == 2: return 2 if peaks[1] - peaks[0] >= 2 else 1
    potentialK = len(peaks)    
    maxFlags = 0 
    while potentialK > maxFlags: 
        cumDistance = 0 
        flags = 0
        firstFlag = True
        for i in range(1, len(distances)):
            cumDistance += distances[i]
            if cumDistance >= potentialK: 
                if firstFlag: 
                    flags += 2 
                    firstFlag = False
                else: 
                    flags += 1
                cumDistance = 0
        if flags > maxFlags and flags == potentialK: 
            return flags 
        elif flags > maxFlags and potentialK > flags: 
            maxFlags = flags
        potentialK -= 1
    return maxFlags 

我设法优化如下:

由于各个标志之间的距离必须 >= 标志的数量,我们知道标志的最大数量将是 peaks 的最后一个元素的根 - peaks 的第一个元素:sqrt(peaks[-1] - peaks[0])

然后我能够将 potentialK 的初始值更新为

potentialK = math.ceil(math.sqrt(peaks[-1] - peaks[0])) 

这应该会大大减少外部 while 循环的迭代次数。

import math 

def solution(A):
    peaks = []
    distances = []
    if len(A) == 1: return 0
    for i in range(1, len(A) -1): 
        if A[i] > A[i-1] and A[i] > A[i+1]:
            if len(distances) == 0: 
                distances.append(i)
            else: 
                distances.append(i - peaks[-1]) 
            peaks.append(i)
    if len(peaks) == 0: return 0
    if len(peaks) == 1: return 1
    if len(peaks) == 2: return 2 if peaks[1] - peaks[0] >= 2 else 1
    potentialK = math.ceil(math.sqrt(peaks[-1] - peaks[0]))  
    maxFlags = 0 
    while potentialK > maxFlags: 
        cumDistance = 0 
        flags = 0
        firstFlag = True
        for i in range(1, len(distances)):
            cumDistance += distances[i]
            if cumDistance >= potentialK: 
                if firstFlag: 
                    flags += 2 
                    firstFlag = False
                else: 
                    flags += 1
                cumDistance = 0
        if flags > maxFlags and flags == potentialK: 
            return flags 
        elif flags > maxFlags and potentialK > flags: 
            maxFlags = flags
        potentialK -= 1
    return maxFlags 

您的算法是 O(n^2),因为输入中可能有 O(n) 个峰值。加快算法速度取决于您提前知道输入大小这一事实。

观察答案是[1, ceil(sqrt(n))]区间内的整数。任何小于 1 的距离要求都意味着您不能放置任何标志。由于距离要求,您不能放置超过 ceil(sqrt(n)) 的标志,即使每个元素都以某种方式成为峰值(这是不可能的)。

因此,您可以进行的一项优化是您只需要检查 O(sqrt(n)) potentialK 值。 (你发布这个作为你自己问题的答案。)这将使运行时间下降到 O(n^(3/2)),因为 m 是 O(n),这显然足够快以通过 Codility 的测试,但是我认为运行时仍然可以改进(正确性也可以)。

我们可以再做一个观察:

存在一个正整数 i 使得:

  1. 对于每个 j,j 是小于 i 的正整数,我们可以放置至少相隔 j 距离的 j 个标志,并且
  2. 对于每个 j,j 是大于 i 的正整数,我们不能放置至少相隔 j 距离的 j 个标志。

这使我们能够使用二进制搜索:

import math

def does_distance_work(peaks, distance):
    peak_count = 1
    last_peak = peaks[0]
    for i in range(len(peaks)):
        if peaks[i] >= last_peak + distance:
            peak_count += 1
            last_peak = peaks[i]
        
    return peak_count >= distance


def solution(A):
    # Get the indices of the peaks.
    peaks = []
    for i in range(1, len(A) - 1):
        if A[i] > A[i - 1] and A[i] > A[i + 1]:
            peaks.append(i)
    
    # Return 0 if no peaks.
    if not peaks:
        return 0
    
    # Check maximum value.
    if does_distance_work(peaks, math.ceil(math.sqrt(len(A)))):
        return math.ceil(math.sqrt(len(A)))

    # If neither of the above two checks apply, find the largest i (as specified above) using binary search.
    low, high = 1, math.ceil(math.sqrt(len(A))) - 1
    while low <= high:
        mid = low + (high - low) // 2
        mid_valid_distance = does_distance_work(peaks, mid)
        mid_plus_one_valid_distance = does_distance_work(peaks, mid + 1)
        if not mid_valid_distance:
            high = mid
        elif mid_plus_one_valid_distance:
            low = mid + 1
        else:
            return mid
    
    # If we've reached this line, something has gone wrong.
    return -1

递归到 O(log(sqrt(n)) 的深度,二分搜索的每次迭代都需要 O(n)。最后的运行时间是 O(n * log(sqrt(n) )),它应该(并且确实)通过了性能测试。