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