Codility Peaks复杂性

时间:2014-01-02 15:47:04

标签: algorithm

我刚刚完成了以下Codility Peaks问题。问题如下:


给出了由N个整数组成的非空零索引数组A. 峰值是一个比其邻居更大的数组元素。更确切地说,它是指数P,使得0 <0。 P&lt; N - 1,A [P - 1]&lt; A [P]和A [P]> A [P + 1]。 例如,以下数组A:

A[0] = 1
A[1] = 2
A[2] = 3
A[3] = 4
A[4] = 3
A[5] = 4
A[6] = 1
A[7] = 2
A[8] = 3
A[9] = 4
A[10] = 6
A[11] = 2

正好有三个峰:3,5,10。 我们希望将此数组划分为包含相同数量元素的块。更准确地说,我们想要选择一个能产生以下块的数字K: A [0],A [1],...,A [K - 1], A [K],A [K + 1],...,A [2K - 1], ... A [N - K],A [N - K + 1],...,A [N - 1]。 更重要的是,每个块应包含至少一个峰值。请注意,块的极端元素(例如A [K-1]或A [K])也可以是峰值,但前提是它们同时具有两个邻居(包括相邻块中的一个)。 目标是找到阵列A可以划分的最大块数。 数组A可按如下方式划分为块:

一个区块(1,2,3,4,3,4,1,2,3,4,6,2)。该块包含三个峰值。

两个区块(1,2,3,4,3,4)和(1,2,3,4,6,2)。每个街区都有一个高峰。

三个块(1,2,3,4),(3,4,1,2),(3,4,6,2)。每个街区都有一个高峰。

特别注意第一个区块(1,2,3,4)在A [3]处有一个峰值,因为A [2]&lt; A [3]&gt; A [4],即使A [4]在相邻的块中。 但是,阵列A不能分为四个块,(1,2,3),(4,3,4),(1,2,3)和(4,6,2),因为(1,2, 3)块不包含峰值。特别注意(4,3,4)块包含两个峰:A [3]和A [5]。 阵列A可以分成的最大块数是三个。

写一个函数: class Solution {public int solution(int [] A); } 在给定由N个整数组成的非空零索引数组A的情况下,返回A可以划分的最大块数。 如果A不能分成若干个块,则该函数应返回0。 例如,给定:

A[0] = 1
A[1] = 2 
A[2] = 3 
A[3] = 4 
A[4] = 3 
A[5] = 4 
A[6] = 1 
A[7] = 2 
A[8] = 3 
A[9] = 4 
A[10] = 6 
A[11] = 2

该函数应返回3,如上所述。 假设:

N是[1..100,000]范围内的整数; 数组A的每个元素都是[0..1,000,000,000]范围内的整数。

复杂度:

预期的最坏情况时间复杂度为O(N * log(log(N)))

预期的最坏情况空间复杂度为O(N),超出输入存储(不计入输入参数所需的存储空间)。

可以修改输入数组的元素。


我的问题

所以我用什么来解决这个问题似乎是强力解决方案 - 从1..N遍历每个组大小,并检查每个组是否至少有一个峰值。我试图解决这个问题的前15分钟,我试图找出一些更优化的方法,因为所需的复杂度是O(N * log(log(N)))。

这是我的“暴力”代码,通过所有测试,包括大型测试,得分为100/100:

public int solution(int[] A) {
    int N = A.length;

    ArrayList<Integer> peaks = new ArrayList<Integer>();
    for(int i = 1; i < N-1; i++){
        if(A[i] > A[i-1] && A[i] > A[i+1]) peaks.add(i);
    }

    for(int size = 1; size <= N; size++){
        if(N % size != 0) continue;
        int find = 0;
        int groups = N/size;
        boolean ok = true;
        for(int peakIdx : peaks){
            if(peakIdx/size > find){
                ok = false;
                break;
            }
            if(peakIdx/size == find) find++;
        }
        if(find != groups) ok = false;
        if(ok) return groups;
    }

    return 0;
}

我的问题是我如何推断这实际上是O(N * log(log(N))),因为它对我来说并不是很明显,我很惊讶我通过了测试用例。我正在寻找最简单的复杂性证明草图,这将使我相信这个运行时。我假设log(log(N))因子意味着在每次迭代时通过平方根来减少问题,但我不知道这是如何适用于我的问题的。非常感谢您的帮助

9 个答案:

答案 0 :(得分:5)

你是完全正确的:要获得日志日志性能,需要减少问题。

python [下面]中的n.log(log(n))解决方案。 Codility不再测试此问题的“性能”(!),但python解决方案的准确性得分为100%。

正如你已经猜测的那样: 外循环将为O(n),因为它正在测试每个块的大小是否是一个干净的除数 内部循环必须是O(log(log(n)))才能给出O(n log(log(n)))。

我们可以获得良好的内循环性能,因为我们只需要执行d(n),即n的除数。我们可以存储 peak-so-far 的前缀和,它使用问题规范允许的O(n)空间。检查每个'组'中是否出现峰值,然后使用组开始和结束索引进行O(1)查找操作。

遵循此逻辑,当候选块大小为3时,循环需要执行n / 3峰值检查。复杂度变为总和:n / a + n / b + ... + n / n,其中分母(a,b,...)是n的因子。

短篇小说: n.d(n)操作的复杂性是O(n.log(log(n)))。

更长的版本: 如果您正在进行Codility课程,您将从Lesson 8: Prime and composite numbers中记住,谐波数运算的总和将给出O(log(n))复杂度。我们有一个减少的集合,因为我们只关注因子分母。 Lesson 9: Sieve of Eratosthenes显示素数的倒数之和如何为O(log(log(n)))并声称'证明是非平凡的'。在这种情况下,Wikipedia告诉我们除数sigma(n)的总和有一个上限(参见Robin的不等式,大约是页面的一半)。

这完全回答了你的问题吗?关于如何改进我的python代码的建议也非常受欢迎!

def solution(data):

    length = len(data)

    # array ends can't be peaks, len < 3 must return 0    
    if len < 3:
        return 0

    peaks = [0] * length

    # compute a list of 'peaks to the left' in O(n) time
    for index in range(2, length):
        peaks[index] = peaks[index - 1]

        # check if there was a peak to the left, add it to the count
        if data[index - 1] > data[index - 2] and data[index - 1] > data[index]:
            peaks[index] += 1

    # candidate is the block size we're going to test
    for candidate in range(3, length + 1):

        # skip if not a factor
        if length % candidate != 0:
            continue

        # test at each point n / block
        valid = True
        index = candidate
        while index != length:

            # if no peak in this block, break
            if peaks[index] == peaks[index - candidate]:
                valid = False
                break

            index += candidate

        # one additional check since peaks[length] is outside of array    
        if index == length and peaks[index - 1] == peaks[index - candidate]:
            valid = False

        if valid:
            return length / candidate

    return 0

<强>现金: @tmyklebu对他SO answer的主要荣誉给了我很多帮助。

答案 1 :(得分:0)

我认为算法的时间复杂度不是O(Nlog(logN))。

然而,它肯定比O(N ^ 2)小得多。这是因为你的内部循环只输入了k次,其中k是N的因子数。整数的因子数可以在这个链接中看到:http://www.cut-the-knot.org/blue/NumberOfFactors.shtml

我可能不准确,但从链接看来,

k ~ logN * logN * logN ...

此外,内环的复杂度为O(N),因为在最坏的情况下峰的数量可以是N / 2.

因此,在我看来,算法的复杂性最多为O(NlogN) ,但它必须足以清除所有测试用例。

答案 2 :(得分:0)

@radicality

至少有一点可以将第二个循环中的遍数优化为O(sqrt(N)) - 收集N的除数并仅迭代它们。

这将使你的算法不那么“蛮力”。

问题定义允许O(N)空间复杂度。你可以存储除数而不会违反这个条件。

答案 3 :(得分:0)

这是我基于前缀和的解决方案。希望对您有所帮助:

class Solution {
    public int solution(int[] A) {
        int n = A.length;
        int result = 1;
        if (n < 3)
            return 0;

        int[] prefixSums = new int[n];
        for (int i = 1; i < n-1; i++)
            if (A[i] > A[i-1] && A[i] > A[i+1])
                prefixSums[i] = prefixSums[i-1] + 1;
            else 
                prefixSums[i] = prefixSums[i-1];
        prefixSums[n-1] = prefixSums[n-2];

        if (prefixSums[n-1] <= 1)
            return prefixSums[n-1];

        for (int i = 2; i <= prefixSums[n-2]; i++) {
            if (n % i != 0)
                continue;
            int prev = 0;
            boolean containsPeak = true;
            for (int j = n/i - 1; j < n; j += n/i) {
                if (prefixSums[j] == prev) {
                    containsPeak = false;
                    break;
                }
                prev = prefixSums[j];                   
            }
            if (containsPeak)
                result = i;
        }

        return result;
    }
}

答案 4 :(得分:0)

def solution(A):
    length = len(A)
    if length <= 2:
        return 0

    peek_indexes = []
    for index in range(1, length-1):
        if A[index] > A[index - 1] and A[index] > A[index + 1]:
            peek_indexes.append(index)

    for block in range(3, int((length/2)+1)):
        if length % block == 0:
            index_to_check = 0
            temp_blocks = 0
            for peek_index in peek_indexes:
                if peek_index >= index_to_check and peek_index < index_to_check + block:
                    temp_blocks += 1
                    index_to_check = index_to_check + block
            if length/block == temp_blocks:
                return temp_blocks

    if len(peek_indexes) > 0:
        return 1
    else:
        return 0
print(solution([1, 2, 3, 4, 3, 4, 1, 2, 3, 4, 6, 2, 1, 2, 5, 2]))

答案 5 :(得分:0)

我刚发现因素时, 然后只需在A中进行迭代并测试所有块数,以查看哪个块划分最大。

这是有100个代码(在Java中)

https://app.codility.com/demo/results/training9593YB-39H/

答案 6 :(得分:0)

一个复杂度为O(N * log(log(N())))的javascript解决方案。

function solution(A) {
    let N = A.length;
    if (N < 3) return 0;
    let peaks = 0;
    let peaksTillNow = [ 0 ];
    let dividers = [];
    for (let i = 1; i < N - 1; i++) {
        if (A[i - 1] < A[i] && A[i] > A[i + 1]) peaks++;
        peaksTillNow.push(peaks);
        if (N % i === 0) dividers.push(i);
    }
    peaksTillNow.push(peaks);
    if (peaks === 0) return 0;
    let blocks;
    let result = 1;
    for (blocks of dividers) {
        let K = N / blocks;
        let prevPeaks = 0;
        let OK = true;
        for (let i = 1; i <= blocks; i++) {
            if (peaksTillNow[i * K - 1] > prevPeaks) {
                prevPeaks = peaksTillNow[i * K - 1];
            } else {
                OK = false;
                break;
            }
        }
        if (OK) result = blocks;
    }
    return result;
}

答案 7 :(得分:0)

使用 C# 代码的解决方案

public int GetPeaks(int[] InputArray)
        { 
            List<int> lstPeaks = new List<int>();
            lstPeaks.Add(0);
            for (int Index = 1; Index < (InputArray.Length - 1); Index++)
            {
                if (InputArray[Index - 1] < InputArray[Index] && InputArray[Index] > InputArray[Index + 1])
                {
                    lstPeaks.Add(1);
                }
                else
                {
                    lstPeaks.Add(0);
                }
            }
            lstPeaks.Add(0);

 
            int totalEqBlocksWithPeaks = 0;
            for (int factor = 1; factor <= InputArray.Length; factor++)
            {
                if (InputArray.Length % factor == 0)
                {
                    int BlockLength = InputArray.Length / factor;
                    int BlockCount = factor;

                    bool isAllBlocksHasPeak = true;
                    for (int CountIndex = 1; CountIndex <= BlockCount; CountIndex++)
                    {
                        int BlockStartIndex = CountIndex == 1 ? 0 : (CountIndex - 1) * BlockLength;
                        int BlockEndIndex = (CountIndex * BlockLength) - 1;

                        if (!(lstPeaks.GetRange(BlockStartIndex, BlockLength).Sum() > 0))
                        {
                            isAllBlocksHasPeak = false;
                        }
                    }

                    if (isAllBlocksHasPeak)
                        totalEqBlocksWithPeaks++; 
                }
            }
            return totalEqBlocksWithPeaks; 
        } 

答案 8 :(得分:-1)

我同意GnomeDePlume的回答......关于在提议的解决方案中寻找除数的部分是O(N),并且可以通过使用课程文本中提供的算法将其减少到O(sqrt(N))

所以只需添加,这是我使用Java解决所需复杂性问题的解决方案。

请注意,它拥有的代码比你的更多 - 一些清理(调试系统和注释)总是可行的: - )

public int solution(int[] A) {
    int result = 0;

    int N = A.length;

    // mark accumulated peaks
    int[] peaks = new int[N];
    int count = 0;
    for (int i = 1; i < N -1; i++) {
        if (A[i-1] < A[i] && A[i+1] < A[i])
            count++;    
        peaks[i] = count;
    }
    // set peaks count on last elem as it will be needed during div checks
    peaks[N-1] = count;

    // check count
    if (count > 0) {
        // if only one peak, will need the whole array
        if (count == 1)
            result = 1;
        else {

            // at this point (peaks > 1) we know at least the single group will satisfy the criteria
            // so set result to 1, then check for bigger numbers of groups
            result = 1;

            // for each divisor of N, check if that number of groups work
            Integer[] divisors = getDivisors(N);

            // result will be at least 1 at this point
            boolean candidate;
            int divisor, startIdx, endIdx;
            // check from top value to bottom - stop when one is found
            // for div 1 we know num groups is 1, and we already know that is the minimum. No need to check.
            // for div = N we know it's impossible, as all elements would have to be peaks (impossible by definition)
            for (int i = divisors.length-2; i > 0; i--) {
                candidate = true;
                divisor = divisors[i];

                for (int j = 0; j < N; j+= N/divisor) {
                    startIdx = (j == 0 ? j : j-1);
                    endIdx = j + N/divisor-1;

                    if (peaks[startIdx] == peaks[endIdx]) {
                        candidate = false;
                        break;
                    }
                }

                // if all groups had at least 1 peak, this is the result!
                if (candidate) {
                    result = divisor;
                    break;
                }
            }

        }
    }

    return result;
}

// returns ordered array of all divisors of N
private Integer[] getDivisors(int N) {
    Set<Integer> set = new TreeSet<Integer>();

    double sqrt = Math.sqrt(N);
    int i = 1;
    for (; i < sqrt; i++) {
        if (N % i == 0) {
            set.add(i); 
            set.add(N/i);
        }
    }
    if (i * i == N)
        set.add(i);

    return set.toArray(new Integer[]{});
}

谢谢, DAVI