2020年5月16日 星期六

Leetcode題解 Python & C#:五月挑戰DAY15 Maximum Sum Circular Subarray

給一個環形陣列 A,要找出總和最大的子陣列。

這題蠻有意思的,讓我沒有順利解出來,有想到好幾種可能,看了別人的解法,發現是用最大最小的方式。
但我想到這方法的當下,沒有去進一步思考。

官方有 Solution ,內有四種解法,但我還是照步調去走從未知去導出解法的思維分析。

如果會做陣列中子陣列最大總和,那環形的也只是稍做變化。

要如何後面接回前面?這是第一個要解決的問題。

對於一個陣列來說,用Python是可以再添加一個一樣的,達到 2n 的長度,在 n - 1 位置之後,又會回到開頭的值。
但由於限制每個元素只能被取一次,這作法可能會讓一元素被選中二次(如全部為正的),並不是一種簡單可行的方法。

又或者弄 linked list ,然後作 DP 以各元素的選取保存每一位置的最大值總和。這能避免重複,但寫來會有點麻煩。

照著 Hint ,要用「 Kadane's algorithm 」, 然而這不是算環形的陣列,需要改良才能用上。如果只跑一遍陣列,那也不會碰到同元素被選二次的狀況。

如果最大值落於陣列間,最後的位置不是 n - 1,那 Kadane's algorithm 就可以找出正解。
但最後位置有碰到 n - 1,那麼就會接回開頭去找的需求。

再思考,如果目前正計算最大的子陣列,沒到計算結束前是不知道目前的是否處在最大子陣列中。而且 Kadane's algorithm 的方式也沒有保存位置,判斷最後位置並不是好的方法。

但以結果回推,最大子陣列以外的元素加總就是最小子陣列。

因此取得最小子陣列的總和,用整個數列的總和去減它,也可以得到最大子陣列的總和。

如果最小子陣列在陣列之間,那最大子陣列會是環形接起的。

因此所求在「陣列(沒有環)子陣列最大總和」或「總合減陣列子陣列最小總和」中,兩者取大。

但這樣子有一個狀況,如果是遞減數列,最小總和會是全體元素集合,而總和減去它會是 0 ,也比最大總和還大。
因此,如果 總和 == 子陣列最小總和 ,那只能回傳前者。

Python
class Solution:
    def maxSubarraySumCircular(self, A: List[int]) -> int:
        min_now = max_now = A[0]        
        nmin = nmax = nsum = A[0]
        for num in A[1:]:
            min_now = min(num, min_now + num)
            nmin = min(nmin, min_now)
            max_now = max(num, max_now + num)
            nmax = max(nmax, max_now)
            nsum += num
        if nsum == nmin: return nmax
        return max(nsum - nmin, nmax)
C#
public class Solution {
    public int MaxSubarraySumCircular(int[] A) {
        int min_now = A[0]; int max_now = A[0];
        int nmin = A[0]; int nmax = A[0]; int nsum = A[0];
        for(int i = 1; i < A.Length; i++)
        {
            int num = A[i];
            min_now = (min_now + num < num) ? min_now + num : num;
            nmin = nmin < min_now ? nmin : min_now;
            max_now = (max_now + num > num) ? max_now + num : num;
            nmax = nmax > max_now ? nmax : max_now;
            nsum += num;
        }
        if(nsum == nmin){return nmax;}
        return (nmax > nsum - nmin) ? nmax : nsum - nmin;
    }
}