pco2699’s blog

学んだものについて、メモしておく場所

LeetCode: Maximum Sum Circular Subarray (Kadane's Algorithm) を解く

問題

May Leetcoding Challange Complementの15日目です。 あっという間に5月の半ばです。

leetcode.com

この問題は「Kadane's Algorithm」の応用問題です。

Kadane's Algrorithmとは

以下のようなマイナスを含むArrayの連続したSubarrayの最大和を求めるアルゴリズムです。

[-1, 3, 4, -10, 8]
=> この場合、[8] が部分和8が最大

[-1, 3, 4, -2, 8]
=> この場合、[3, 4, -2, 8]で部分和13が最大

Kadane's Algorithmとはこの問題を動的計画法で解く問題です。

以下の漸化式を立てれば、答えが導き出せます。

対象のArrayをnumとする。numのi番目の数字をnum[i]とする。
dp[i] = max(dp[i-1] + num[i], num[i])

この数式はどういう意味かというと、num を一番目から順番に足し合わせていって、足し合わせるたびに以下の2つの選択肢を考えます。

  1. 前からの和を引きつぐか dp[i-1] + num[i]
  2. 前からの和を捨てて、自分の数から仕切り直すか num[i]

マイナスの数が多すぎて、今までの和があまりに小さい場合、仕切り直したほうが和が大きくなる場合があります。

以下のような例です。(いつものようにクソ雑な図。今回からタブレットを使ってみました。) f:id:pco2699:20200515222455p:plain

これをコードに起こすと以下の形になります。

def kadane(nums):
  dp = [0] * len(nums)
  dp[0] = nums[0]
  for i in range(1, len(nums)):
    dp[i] = max(dp[i-1] + nums[i], nums[i])

  return dp[-1]

これだと 空間計算量が O(n) です。 実際に必要なのは dp[i-1] だけなので、これのみを持つ変数をループごとに更新していけば O(1)で済みます。

def kadane(nums):
  dp = nums[0]
  maxSum = nums[0]
  for i in range(1, len(nums)):
    dp = max(dp + nums[i], nums[i])
    maxSum = max(maxSum, dp)

  return maxSum

解き方

先ほども説明した通り、この問題はこの Kadane's Algorithmの応用です。

ただし問題となる配列が循環する配列のため、以下の2パターンに分けて考える必要があります。

  1. 部分和が一つの配列内で完結するパターン 例: [[3,-2,2],-3] => [3,-2,2] が部分和
  2. 部分和が配列を跨ぐパターン 例: [5,-3,5] => [5, -3, 5] が部分和

この1のパターンは、普通にKadane's Algorithmで求めることができます。 2のパターンをどうやって出すか?がこの問題の鬼門です。

2パターンを考えると、以下の図のように、全体から真ん中の配列だけ差し引けばよいことがわかります。

f:id:pco2699:20200518000433p:plain

この差し引く配列の数が小さい=マイナスをかけて、最大のもの部分長配列を求める問題、と言い換えることができます。

ただし、すべての配列を選んでしまうと、空の配列を選ぶことになってしまうため、最初と最後の数をそれぞれ除いた2パターンでKadane's Algroithmを行います。

回答

class Solution:
    def maxSubarraySumCircular(self, A: List[int]) -> int:
        def kadane(nums: List[int]):
            if not nums:
                return 0
            dp = nums[0]
            maxSum = nums[0]
            for i in range(1, len(nums)):
                dp = max(dp + nums[i], nums[i])
                maxSum = max(maxSum, dp)

            return maxSum
        
        maxSum = kadane(A)
        maxSum1 = sum(A) + kadane([-1 * A[i] for i in range(1, len(A))])
        maxSum2 = sum(A) + kadane([-1 * A[i] for i in range(0, len(A)-1)])
        return max(maxSum, maxSum1, maxSum2)

計算量

時間計算量: O(N)
空間計算量: O(1)