算法系列-最大子數組的幾種算法剖析


問題描述:
        給定一只股票在某段時間內的歷史價格變化曲線,找出一個能夠實現收益最大化的時間段。
    
理解:
        為找出最大化的收益,需要考慮的是在買進和賣出時的價格變化幅度,因此從該股票的每日變化幅度來考慮問題比較合適。由此,可以將上述問題稍作變形:給定一只股票在某段時間內的每日變化幅度,找出一個合適的買進和賣出時間,以實現收益最大化。因此,將輸入數據轉換如下,並試圖在整個時間段中找到一個累加和最大的子區間,亦即最大子數組。
    
暴力求解方法:
        首先能夠想到的是在一個給定數組(區間)中,其子數組(子區間)的個數是C(2,n),很容易就能遍歷完所有子數組從而找出最大的那個,其最壞情況漸進時間復雜度是Θ(n2)。假設每日變化幅度保存在數組A中(A的下標從1到n),A.length表示A的元素個數,最終結果以元組形式返回;給出偽碼如下:
        BRUTE_FORCE(A)
            i = 1
            sum = -infinity
            for i <= A.length, inc by 1
                j = i
                last_sum = 0
                for j <= A.length, inc by 1
                    last_sum += A[j]
                    if last_sum > sum
                        sum = last_sum
                        start = i
                        end = j
            return (start, end, sum)


分治求解方法:
        上述方法的漸進時間復雜度差強人意。類比於歸並排序,有時采用分治策略能夠獲得更好的時間復雜度。分治策略通常包含分解成子問題、解決子問題、合並子問題。由此可以推出大致的解決思路:首先依然假設數據輸入如上一個方法那樣,然后考慮將A[1...n]拆分為規模大致相同的兩個子數組left[1...mid]和right[mid+1...n],其中mid=(1+n)/2向下取整,那么可以肯定,最大子數組要么在這兩個子數組中,要么橫跨這兩個子數組,因此可以分別求解這三種情況,取其中最大的子數組並返回即可。
        對於left/right子數組可遞歸求解,而對於橫跨兩個子數組的情況,如果能夠使得該情況下的求解時間復雜度為O(n),那么應該能讓整體的最壞時間復雜度低於Θ(n2)。如果僅僅是通過遍歷所有包含A[mid]和A[mid+1]的子數組來找最大子數組,那么很顯然僅求解該情況就需要Θ(n2)的時間。可以推斷橫跨兩個子數組的最大子數組,必須由兩個分別在left/right中的子數組組成,這兩個子數組在分別包含了A[mid]和A[mid+1]的所有子數組中是最大的;因為如果存在一個不滿足上述條件的最大子數組,那么總可以用上述方法找到一個更大的子數組。
        根據上述思路,很容易推知求解橫跨兩個子數組的情況只需要O(n)的時間。由此給出偽碼如下:
        (1)子過程:找出橫跨兩個子數組的最大子數組
            FIND_CROSSING_MAX_SUBARRAY(A, low, mid, high)
                left_sum = -infinity
                sum = 0
                i = mid
                for i >= low, dec by 1
                    sum += A[i]
                    if sum > left_sum
                        left_sum = sum
                        left_index = i
                
                right_sum = -infinity
                sum = 0
                i = mid + 1
                for i <= high, inc by 1
                    sum += A[i]
                    if sum > right_sum
                        right_sum = sum
                        right_index = i
                return (left_index, right_index, left_sum+right_sum)
        
        (2)主過程:分治法找出最大子數組
            FIND_MAX_SUBARRAY(A, low, high)
                if low == high
                    return (low, high, A[low])
                else
                    mid = down_trunc((low + high) / 2)
                    (left_start, left_end, left_sum) =
                        FIND_MAX_SUBARRAY(A, low, mid)
                    (right_start, right_end, right_sum) =
                        FIND_MAX_SUBARRAY(A, mid+1, high)
                    (cross_start, cross_end, cross_sum) =
                        FIND_CROSSING_MAX_SUBARRAY(A, low, mid, high)
                    
                    if left_sum > right_sum and left_sum > cross_sum
                        return (left_start, left_end, left_sum)
                    else if right_sum > left_sum and right_sum > cross_sum
                        return (right_start, right_end, right_sum)
                    else
                        return (cross_start, cross_end, cross_sum)
        可以看出上述算法漸進時間復雜度為Θ(nlg(n))。


縮減問題規模的方法:
        在查找過程中,是否可以根據現有的信息,來縮減需要排查的子數組個數,進而獲得更好的時間復雜度呢?一個思路是不再重復檢查以前累加過的元素,即從左至右累加元素,保存其中的最大子數組,如果在加入一個元素后累加和為負數,則從該元素的后一個元素重新累加,直至整個數組遍歷完畢。該思路有效的前提是證明以下幾個假設:

  1. 可以將最大子數組來源分為三種:已經遍歷完的數組部分、未遍歷的數組部分以及跨越這兩部分的子數組
  2. 可以假設當從左至右累加直至累加和為負,所得的最大子數組是當前已遍歷完的數組部分中最大的
  3. 可以假設當累加和為負時,潛在的最大子數組不可能從該元素或該元素左邊的元素開始

        假設1不證自明。
        假設從A[1]累加到A[i]時第一次遇到其累加和為負(1<=i<=n),那么A[i]一定為負,且A[1]+...+A[i-1]>=0。當i<=2時,顯然此時假設2成立。當i>2時,可以認為在A[1]...A[i]中,所有子數組可分為三種:從A[1]開始向右拓展、從A[i]開始向左拓展以及不包含A[1]和A[i]的中間子數組;顯然從A[i]向左拓展的不可能是最大子數組,而如果不包含A[1]和A[i]的中間子數組是最大子數組,那么可以使該中間子數組加上其左邊的部分構成一個新的子數組,而且該子數組總是大於等於這個中間子數組,因為其左邊部分總是大於等於0,所以該情況下假設2也得證。綜合來看假設2是成立的。
        對於假設3,顯然潛在的最大子數組不可能從A[i]開始,因為A[i]<0。當潛在的最大子數組從A[i]的左邊開始時,假設其從A[j]開始(1<=j<i)。顯然j不能等於1,因為A[1]+...+A[i]<0;當j>1時,A[j]+...+A[i]一定是負數,因為A[1]+...+A[j-1]一定大於等於0而A[1]+...+A[i]一定為負。所以綜合來看,從A[i]或者A[i]的左邊尋找潛在的子數組是沒有意義的。
        偽碼如下,時間復雜度為Θ(n)。對於全部是負數的情況,特殊處理即可,不影響時間復雜度。
        LINEAR_SEARCH_MAX_SUBARRAY(A)
            sum = -infinity
            start = 0
            end = 0

            cur_sum = 0
            cur_start_index = 1

            i = 1
            for i <= A.length, inc by 1
                cur_sum += A[i]
                if cur_sum < 0
                    cur_sum = 0
                    cur_start_index = i + 1
                else
                    if sum < cur_sum
                        sum = cur_sum
                        start = cur_start_index
                        end = i

            return (start, end, sum)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM