【算法】最大子數組問題

問題描述:
        給定一隻股票在某段時間內的歷史價格變化曲線,找出一個能夠實現收益最大化的時間段。
    
理解:
        爲找出最大化的收益,需要考慮的是在買進和賣出時的價格變化幅度,因此從該股票的每日變化幅度來考慮問題比較合適。由此,可以將上述問題稍作變形:給定一隻股票在某段時間內的每日變化幅度,找出一個合適的買進和賣出時間,以實現收益最大化。因此,將輸入數據轉換如下,並試圖在整個時間段中找到一個累加和最大的子區間,亦即最大子數組。
    
暴力求解方法:
        首先能夠想到的是在一個給定數組(區間)中,其子數組(子區間)的個數是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)

轉載自:https://www.cnblogs.com/SyBlog/p/11371922.html#commentform

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