快速的最大归一化子数组总和算法?

时间:2019-04-21 08:28:06

标签: algorithm

最大子数组和问题具有非常简单的线性时间解https://en.m.wikipedia.org/wiki/Maximum_subarray_problem

如果相反,我们想最大化sum(subarray)/ sqrt(subarray length),那么是否存在次二次时间解?

输入数组的元素将是-infinity到+ infinity范围内的浮点值。

3 个答案:

答案 0 :(得分:4)

更新

我在以下测试中添加了estabroo的基于Kadane的代码版本。在我的测试中似乎显示出最多10%的差异(运行摘要进行随机测试)。

(结束更新)

我能想到的最好的办法是,在搜索O(log m * n * num_samples_constant)期间对目标进行二进制搜索,并随机选择窗口大小的样本,其中m是范围。在测试中,我看到了蛮力(限于5000个元素数组,范围为±1000000000)之间的变化,而蛮力在0到30%之间变化,样本大小为200个窗口长度。 (也许另一个例程可以进一步完善?)

下面的JavaScript代码运行10个测试,并报告最小和最大差异,然后仅在较长的数组上进行二进制搜索。

其他想法包括使用FFT生成和,但我不知道是否有一种有效的方法可以将每个和与生成它的子数组长度相关联;并尝试找到问题的另一种表示形式:

f = sqrt(i - j) * (si - sj), for j < i
f^2 = sqrt(i - j) * (si - sj) * sqrt(i - j) * (si - sj)
    = (i - j) * (si^2 - 2si*sj + sj^2)
    = i*si^2 - 2i*si*sj + i*sj^2
      -j*si^2 + 2j*si*sj - j*sj^2

    = i*si^2 + 
      (-2sj, sj^2, -j, 2j*sj, -j*sj^2) // known before i
        dot (i*si, 1, si^2, si, 1)

(因此,如果我们在对数时间中解决了5维凸包更新,5维极点问题,并弄清楚我们的候选对象是正数还是负数,我们会很高兴:)

function prefix_sums(A){
  let ps = new Array(A.length + 1).fill(0)
  for (let i=0; i<A.length; i++)
    ps[i + 1] = A[i] + ps[i]
  return ps
}

function brute_force(ps){
  let best = -Infinity
  let best_idxs = [-1, -1]
  for (let i=1; i<ps.length; i++){
    for (let j=0; j<i; j++){
      let s = (ps[i] - ps[j]) / Math.sqrt(i - j)
      if (s > best){
        best = s
        best_idxs = [j, i - 1]
      }
    }
  }
  return [best, best_idxs]
}

function get_high(A){
  return A.reduce((acc, x) => x > 0 ? acc + x : acc, 0)
}

function get_low(A){
  return Math.min.apply(null, A)
}


function f(A){
  let n = A.length
  let ps = prefix_sums(A)
  let high = get_high(A)
  let low = get_low(A)
  let best = [-1, -1]
  let T = low + (high - low) / 2
  let found = false

  while (low + EPSILON < high){
    T = low + (high - low) / 2
    // Search for T
    found = false

    for (let l=0; l<NUM_SAMPLES; l++){
      let w = Math.max(1, ~~(Math.random() * (n + 1)))

      for (let i=w; i<ps.length; i++){
        let s = (ps[i] - ps[i - w]) / Math.sqrt(w)
        if (s >= T){
          found = true
          best = [i - w, i - 1]
          break
        }
      }
      if (found)
        break
    }
    // Binary search
    if (found)
      low = T
    else
      high = T - EPSILON 
  }

  return [low, best]
}

function max_subarray(A){
    var max_so_far = max_ending_here = A[0]
    var startOld = start = end = 0
    var divb = divbo = 1
    //for i, x in enumerate(A[1:], 1):
    for (let i=1; i<A.length; i++){
        var x = A[i]
        divb = i - start + 1
        divbo = divb - 1
        if (divb <= 1){
            divb = 1
            divbo = 1
        }
        undo = max_ending_here * Math.sqrt(divbo)
        max_ending_here = Math.max(x, (undo + x)/Math.sqrt(divb))
        if (max_ending_here == x)
            start = i
        max_so_far = Math.max(max_so_far, max_ending_here)
        if (max_ending_here < 0)
            start = i + 1
        else if (max_ending_here == max_so_far){
            startOld = start
            end = i
        }
    }
    if (end == A.length-1){
        start = startOld + 1
        var new_max = max_so_far
        divbo = end - startOld + 1
        divb = divbo - 1
        while (start < end){
            new_max = (new_max * Math.sqrt(divbo) - A[start-1])/Math.sqrt(divb)
            if (new_max > max_so_far){
                max_so_far = new_max
                startOld = start
            }
            start += 1
        }
    }
    return [max_so_far , startOld, end]
}

const EPSILON = 1
const NUM_SAMPLES = 200

let m = 1000000000
let n = 5000
let A

let max_diff = 0
let min_diff = Infinity
let max_diff2 = 0
let min_diff2 = Infinity
let num_tests = 10

for (let i=0; i<num_tests; i++){
  A = []
  for (let i=0; i<n; i++)
    A.push([-1, 1][~~(2 * Math.random())] * Math.random() * m + Math.random())

  let f_A = f(A)
  let g_A = brute_force(prefix_sums(A))
  let m_A = max_subarray(A)
  let diff = (g_A[0] - f_A[0]) / g_A[0]
  max_diff = Math.max(max_diff, diff)
  min_diff = Math.min(min_diff, diff)
  let diff2 = (g_A[0] - m_A[0]) / g_A[0]
  max_diff2 = Math.max(max_diff2, diff2)
  min_diff2 = Math.min(min_diff2, diff2)
}

console.log(`${ n } element array`)
console.log(`${ num_tests } tests`)
console.log(`min_diff: ${ min_diff * 100 }%`)
console.log(`max_diff: ${ max_diff * 100 }%`)
console.log(`min_diff (Kadane): ${ min_diff2 * 100 }%`)
console.log(`max_diff (Kadane): ${ max_diff2 * 100 }%`)

n = 100000
A = []
for (let i=0; i<n; i++)
  A.push([-1, 1][~~(2 * Math.random())] * Math.random() * m + Math.random())

var start = +new Date()
console.log(`\n${ n } element array`)
console.log(JSON.stringify(f(A)))
console.log(`${ (new Date() - start) / 1000 } seconds`)

答案 1 :(得分:2)

有趣的问题。由于您提到了对近似的兴趣,因此这是一种在O(nε -1 )时间内运行的1-O(ε)近似方案。它具有仅使用+和max的良好特性,避免了通过减去前缀和而带来的灾难性抵消问题。 (由于输入包含负数,因此仍然有可能发生灾难性的抵消,但随后我们可以解决只包含所涉及的大正整数的子数组。)

让k = ceil(ε -1 )。在O(nε −1 )时间内,我们可以使用简单算法评估长度在1到k之间的每个子数组。我们通过迭代粗化输入并运行基本相同的算法,以近似的方式评估更长的子数组。由于输入在每次迭代中都会缩小一个恒定因子,因此总运行时间为O(nε -1 )。

粗化步骤如下。我们保留三个相同长度的派生数组。派生数组中的每个位置对应于原始输入中长度为2 的子数组。三个派生数组是S(每个元素是相应子数组后缀的最大和),A(每个元素是相应子数组的后缀的最大)和P(每个元素是相应子数组的前缀的最大和)。 )。考虑对于i ℓ(j−i-1)+ 2和2 (j−i + 1)之间。这足以限制目标(如果总和为正,则使用后者作为元素数量的估计;如果总和为负,则使用前者),因此可以将其与其他子数组进行近似比较。

要在每次迭代中从S,A,P导出S',A',P',我们计算S'[i] = max(S [2i] + A [2i + 1],S [2i + 1 ])和A'[i] = A [2i] + A [2i + 1]和P'[i] = max(P [2i],A [2i] + P [2i + 1])。如果索引2i存在但2i + 1不存在,则将S'[2i] = S [2i]设置为A'[2i] = A [2i],将P'[2i] = P [2i]设置。

1-O(ε)近似的证明草图是,给定最佳子数组,我们找到最小的ℓ,使得其长度最大为2 ℓ-1 k。然后我们看一下迭代ℓ,找到i和j,观察到S [i] + A [i + 1] +…+ A [j-1] + P [j]至少等于最佳子阵列,并将因分母取整而导致的损失乘以1 + O(ε)的乘积因子。

答案 2 :(得分:1)

由于(a + b)/ sqrt(n)与a / sqrt(n)+相同,因此Wikipedia页面中显示的Kadane算法(第二个显示跟踪子数组的开始和结束)也应对此起作用。 b / sqrt(n)。因此,您无需取消前一个除法,而无需添加完整的值(max_end_here + x),而是添加新的值,然后除以新长度的平方根。

DatasetB

enter image description here