【问题标题】:Codility Peaks ComplexityCodility 峰值复杂性
【发布时间】:2014-01-02 15:47:04
【问题描述】:

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


给出了一个由 N 个整数组成的非空零索引数组 A。 一个峰值是一个比它的邻居大的数组元素。更准确地说,它是一个索引 P,使得 0 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] 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最多可以分成三个块。

写一个函数: 类解决方案{公共int解决方案(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)) 因子意味着在每次迭代中通过平方根来某种程度地减少问题,但我不知道这如何适用于我的问题。非常感谢您的帮助

【问题讨论】:

  • 将 peakIdx 除以大小并不那么聪明,因为二进制数的除法涉及大量的加法 + 移位操作。检查峰值索引是否在当前块的边界索引内要快得多,因为这使用 2 个减法和 1 个逻辑比较操作。

标签: algorithm


【解决方案1】:

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

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

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

我们可以获得良好的内循环性能,因为我们只需要执行 d(n),即 n 的除数。我们可以存储 peaks-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 给予了极大的赞誉,这对我帮助很大。

【讨论】:

    【解决方案2】:

    我不认为你的算法的时间复杂度是 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)充其量,但它必须足以清除所有测试用例。

    【讨论】:

    • 您是否知道问题所期望的 Nloglog(N) 解决方案是什么? (Codility 确实运行该程序并声称我的解决方案是 Nloglog(N),但我猜想在不使用非常大的数据集的情况下,很难从统计上区分 Nloglog(N) 解决方案和 NlogN)
    【解决方案3】:

    @激进

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

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

    问题定义允许 O(N) 空间复杂度。您可以在不违反此条件的情况下存储除数。

    【讨论】:

      【解决方案4】:

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

      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;
          }
      }
      

      【讨论】:

        【解决方案5】:
        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]))
        

        【讨论】:

          【解决方案6】:

          一开始我只是发现了因素, 然后在 A 中迭代并测试所有块数,看看哪个是最大的块划分。

          这是得到 100 的代码(在 java 中)

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

          【讨论】:

            【解决方案7】:

            复杂度为 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;
            }
            

            【讨论】:

              【解决方案8】:

              使用 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; 
                      } 
              

              【讨论】:

                【解决方案9】:

                这个任务实际上有一个 O(n) 运行时复杂度的解决方案,所以这是一个谦虚的尝试来分享它。

                从建议的 O(n * loglogn) 解决方案到 O(n) 的诀窍是计算任意两个峰(或对应端点的前导或尾随峰)之间的最大间隙。

                这可以在第一个 O(n) 循环中构建峰值哈希时完成。

                然后,如果两个连续峰之间的间隔为“g”,则最小组大小必须为“g/2”。它只是开始和第一个峰值之间的“g”,或最后一个峰值和结束之间的“g”。此外,从组大小为“g”的任何组中至少会有一个峰,因此要检查的范围是:g/2、1+g/2、2+g/2、... g。

                因此,运行时间是 d = g/2, g/2+1, ... g) * n/d 的总和,其中“d”是除数。

                (d 的总和 = g/2, 1 + g/2, ... g) * n/d = n/(g/2) + n/(1 + g/2) + ... + (n/g)

                如果 g = 5,则这个 n/5 + n/6 + n/7 + n/8 + n/9 + n/10 = n(1/5+1/6+1/7+1/8 +1/9+1/10)

                如果你用最大的元素替换每一项,那么你得到 sum

                现在,概括一下,每个元素都替换为 n / (g/2)。

                从 g/2 到 g 的项目数是 1 + g/2,因为有 (g - g/2 + 1) 个项目。

                所以,总和是:n/(g/2) * (g/2 + 1) = n + 2n/g

                因此,操作总数的界限是 O(n)。

                用 C++ 实现的代码在这里:

                int solution(vector<int> &A)
                {
                  int sizeA = A.size();
                  vector<bool> hash(sizeA, false);
                  int min_group_size = 2;
                 
                  int pi = 0; 
                  for (int i = 1, pi = 0; i < sizeA - 1; ++i) {
                    const int e = A[i];
                    if (e > A[i - 1] && e > A[i + 1]) {
                      hash[i] = true;
                      int diff = i - pi;
                      if (pi) diff /= 2;
                      if (diff > min_group_size) min_group_size = diff;
                      pi = i;
                    }
                  }
                  min_group_size = min(min_group_size, sizeA - pi);
                
                  vector<int> hash_next(sizeA, 0);
                  for (int i = sizeA - 2; i >= 0; --i) {
                    hash_next[i] = hash[i] ? i : hash_next[i + 1];
                  }
                
                  for (int group_size = min_group_size; group_size <= sizeA; ++group_size) {
                    if (sizeA % group_size != 0) continue;
                    int number_of_groups = sizeA / group_size;
                    int group_index = 0;
                    for (int peak_index = 0; peak_index < sizeA; peak_index = group_index * group_size) {
                      peak_index = hash_next[peak_index];
                      if (!peak_index) break;
                      int lower_range = group_index * group_size;
                      int upper_range = lower_range + group_size - 1;
                      if (peak_index > upper_range) {
                        break;
                      }
                      ++group_index;
                    }
                
                    if (number_of_groups == group_index) return number_of_groups;
                  }
                
                  return 0;
                }
                

                【讨论】:

                  【解决方案10】:

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

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

                  请注意,它的代码比你的多得多 - 总是可以进行一些清理(调试 sysouts 和 cmets):-)

                  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[]{});
                  }
                  

                  谢谢, 戴维

                  【讨论】:

                  • 这个问题要求解释提问者的代码,而不是其他实现。
                  • 你是对的......但 GnomeDePlume 已经回答了关于复杂性的问题。我的目的是提供一个 Java 实现,以正确的复杂性解决问题。我会改变答案以反映这一点。感谢您指出这一点!
                  猜你喜欢
                  • 2019-03-04
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-08-24
                  • 2019-02-14
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-10-09
                  • 1970-01-01
                  相关资源
                  最近更新 更多