從「最大區間和問題」看算法的精進

最早知道「最大區間和問題」好像是看 Udi Manber 的《算法引論》。最近又在 @吳軍 的《計算之魂》1.3 節中看到。

nums = %w[1.5 -12.3 3.2 -5.5 23.2 3.2 -1.4 -12.2 34.2 5.4 -7.8 1.1 -4.9].map { |e| e.to_f }

O(n^3) 算法

def max_sum_1(ary)
    n = ary.size
    max_sum = 0
    b, e = 0, 0

    n.times do |p|
        (p...n).each do |q|
            # s = ary[p..q].sum
            # 爲便於分析算法性能,還是用 for 循環更清晰一些:
            s = 0
            ary[p..q].each { |e| s += e }

            if s > max_sum
                max_sum = s
                b, e = p, q
            end
        end
    end
    [max_sum, [b, e]]
end

p max_sum_1(nums) #=> [52.4, [4, 9]]

O(n^2) 算法

def max_sum_2(ary)
    n = ary.size
    max_sum = 0
    b, e = 0, 0

    n.times do |p|
        partial_sum = 0     # 保存 ary[p..(q - 1)].sum
        (p...n).each do |q|
            # puts "#{p} .. #{q}"
            s = partial_sum + ary[q]

            if s > max_sum
                max_sum = s
                b, e = p, q
            end

            partial_sum = s
        end
    end
    [max_sum, [b, e]]
end

p max_sum_2(nums)

O(n log(n)) 分治算法

def max_sum_3_using_divide_and_conquer(ary, b, e)
    return [ary[b], [b, e]] if b == e

    # -----b_l----e_l----------(mid)---------b_r---------e_r-----➡️
    mid = (b + e) / 2

    max_sum_l, interval = max_sum_3_using_divide_and_conquer(ary, b, mid)
    b_l, e_l = interval

    max_sum_r, interval = max_sum_3_using_divide_and_conquer(ary, mid + 1, e)
    b_r, e_r = interval

    if e_l + 1 == b_r
        if max_sum_l > 0 and max_sum_r > 0
            return [max_sum_l + max_sum_r, [b_l, e_r]]
        else
            if max_sum_l > max_sum_r
                return [max_sum_l, [b_l, e_l]]
            else
                return [max_sum_r, [b_r, e_r]]
            end
        end
    else
        maybe_sum = max_sum_l + ary[(e_l + 1)..(b_r - 1)].sum + max_sum_r
        sum2p = {
            maybe_sum => [b_l, e_r],
            max_sum_l => [b_l, e_l],
            max_sum_r => [b_r, e_r]
        }

        larger_sum = [maybe_sum, max_sum_l, max_sum_r].max
        return [larger_sum, sum2p[larger_sum]]
    end
end

p max_sum_3_using_divide_and_conquer(nums, 0, nums.size - 1)

終極皇冠👑:O(n) 線性算法

@吳軍 說是用「正、反兩遍掃描」,但理解起來太費勁。其實用遞歸也能解釋(先不考慮邊界情況):

  1. 找到第一個爲正 + 的數 p_0 ,再找 p_0 之後第一個爲負 - 的數 q_0 。計算出 \sum [p_0, q_0) = S_0
  2. 遞歸尋找 [q_0, e] 段上的最大區間和,記爲 \sum [p_k, q_k) = S_k ,同時順道求出 \sum [q_0, p_k) = \tilde{S}
  3. 剩下的就是常規操作了:
    I. \quad \ \ if \ S_0 + \tilde{S} \ge 0, then \ S_0 + \tilde{S} + S_k \ge S_k ,意味着排除掉 S_k ,需要在 max{ \{ S_0, \ S_0 + \tilde{S} + S_k \} } 中選擇。
    II. \quad if \ S_0 + \tilde{S} \lt 0, then \ S_0 + \tilde{S} + S_k \lt S_k ,意味着只需在 max{ \{ S_0, \ S_k \} } 中選擇。

不過坑🕳️還是很多的,昨晚調試到凌晨兩點🕑😖。

def find_the_first_positive_num(ary, b = 0)
  l = nil
  sum_before_l = 0
  ary[b..].each_with_index do |e, i|
    if e >= 0
      l = b + i
      break
    end
    sum_before_l += e
  end

  if l.nil?   # 全爲負。
    return [nil, nil]
  else
    return [l, sum_before_l]
  end
end

def find_the_first_negative_num(ary, b = 0)
  l = nil
  sum_before_l = 0
  ary[b..].each_with_index do |e, i|
    if e < 0
      l = b + i
      break
    end
    sum_before_l += e
  end

  if l.nil?   # 全爲正。
    return [nil, nil]
  else
    return [l, sum_before_l]
  end
end

def max_sum_4(ary, b, e, depth = 0)
  # return format: [sum_before_l, [l..r], max_sum]
  return [0, [b, e], ary[b]] if b == e if b == e
    
  p0, sum_left_side_0 = find_the_first_positive_num(ary, b)
  
  if p0.nil?  # 全爲負數。
    max_negative = ary[b..e].max
    idx = ary.index(max_negative)

    return [0, [idx, idx], max_negative]
  else
    l, r, max_sum = nil

    q0, sum_p0 = find_the_first_negative_num(ary, p0)

    if q0.nil?
      return [ary[b...p0].sum, [p0, e], ary[p0..e].sum] if q0.nil?  # [p0 .. e] 全爲正數。
    else
      sum_left_side, interval, max_sum_rest = max_sum_4(ary, q0, e, depth)
      l_rest, r_rest = interval

      return [sum_left_side_0, [p0, q0 - 1], sum_p0] if max_sum_rest < 0

      if sum_p0 + sum_left_side >= 0
        if sum_left_side + max_sum_rest >= 0
          # 擴展至 [p0 .. r_rest]
          return [sum_left_side_0, [p0, r_rest], sum_p0 + sum_left_side + max_sum_rest]
        else
          # 只保留 [p0 .. q0 - 1]
          return [sum_left_side_0, [p0, q0 - 1], sum_p0]
        end
      else
        if sum_p0 >= max_sum_rest
          return [sum_left_side_0, [p0, q0 - 1], sum_p0]
        else
          return [sum_left_side_0 + sum_p0 + sum_left_side, [l_rest, r_rest], max_sum_rest]
        end
      end
    end
  end
end

p max_sum_4(nums, 0, nums.size - 1)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章