zwdnet同學在下面的貼子提到了如下問題:
“求助:求一個整數數組的最小子序列之和結果出錯
是《算法設計與分析基礎》一書的一道習題,原題是要求一個整數數組中大小相差最小的兩個元素之差(只要求差值),題目給出了窮舉的方法,並要求改進。我在網上搜了,有人給出了答案,但是沒有代碼(見http://fayaa.com/tiku/view/116/),我按其方法用c++試了試,答案和用窮舉法做的不一致,而且相差很大。麻煩各位幫忙看看。謝謝。”
http://topic.csdn.net/u/20091107/11/dc280e49-43b1-4a6d-800c-49a799ab5ea1.html?7444
我思索了一下,覺得這個問題與我先前看過的一些問題有關聯,很值得研究,所以就把它整理出來,寫在我的博客裏了。(這個問題並沒有完全解決)
首先要弄懂“最大子段和”這個問題,這個問題在劉汝佳的上課講義上有,我就直接COPY過來了,如下:
題目:給一串整數a[1…n],求出它和最大的子序列,即找出1<=i<=j<=n,使a[i]+a[i+1]+…+a[j]最大。
介紹四個算法並分析時間複雜度
枚舉:O(n3)
優化的枚舉:O(n2)
分治:O(nlogn)
貪心:O(n)
問題的解答其實只與算法四“貪心算法”有關,但是爲了保持敘述的完整性,把算法一至自算法三都寫出來了。
算法一(窮舉):
思想:枚舉所有的i和j,計算a[i]+…+a[j],選擇最大的
程序如下:
max := a[1];
for i:=1 to n do
for j:=i to n do begin
sum := 0;
for k:=i to j do
sum := sum + a[k];
if sum > max then max := sum;
end;
時間複雜度爲O(n3),顯而易見。
算法二(優化的窮舉):
思路
枚舉i和j後,能否快速算出a[i]+…+a[j]呢?
能,預處理!
記s[i] = a[1] + a[2] + … + a[i], 規定s[0] = 0
則可以在O(n)的時間內計算出s[1], …, s[n]
s[0] := 0;
for i:=1 to n do
s[i] := s[i–1] + a[i];
有了s[i],怎麼快速求a[i]+…+a[j]呢?
a[i]+…+a[j]
= (a[1] + … + a[j]) – (a[1] + … + a[i-1])
=s[j] – s[i-1]
而s[i]經過預處理以後可以直接讀出!
程序如下:
max := a[1];
for i:=1 to n do
for j:=i to n do
if s[j] – s[i-1] > max then
max := s[j] – s[i-1];
時間複雜度:預處理+主程序=O(n+n2)=O(n2)
算法三(分治):
用一種完全不同的思路
最大子序列的位置有三種可能
1.完全處於序列的左半,即j<=n/2
2.完全處於序列的右半,即i>=n/2
3.跨越序列中間,即i<n/2<j
用遞歸的思路解決!
設max(i, j)爲序列a[i…j]的最大子序列
設mid = (i + j)/2,即區間a[i…j]的中點
最大的第一種序列爲max(i, mid)
最大的第二種序列爲max(mid+1, j)
問題:最大的第三種序列爲???
既然跨越中點,把序列a[i…j]劃分爲兩部分
a[i…mid]:最大序列用掃描法在n/2時間內找到
一共只有mid-1=n/2種可能的序列,一一比較即可
a[mid+1…j]:同理
一共花費時間爲j-i+1
分治算法時間複雜度:O(nlogn)
算法四(貪心):
(加紅色部分是解決問題的關鍵)
算法二的實質是求出i<=j,讓s[j]-s[i-1]最大
對於給定的j,能否直接找到在j之前的最小s值呢?
從小到大掃描j
j=1時,只有一個合法的i,即i=1, s[1]=0
如果s[j]變大,則最小的s值和上次一樣
如果s[j]再創新低,應該讓s[j]作爲今後的最優s值
min_s := 0;
For j :=1 to n do begin
if s[j] < min_s then min_s := s[j];
if s[j] – min_s > max then
max := s[j] – min_s;
End;
時間複雜度很明顯:O(n)
舉個例子來看:
a[1..9] : 5, 6, -7, -8, 4, 9, 8,-6, 7;
s[1..9]: 5,11,4,-4,0,9,17,11,18
上述標誌爲紅色的代碼運行過程如下:
min_s=0;
循環9次,兩個變量的變化情況:
mis_s: 0,0,0,-4,-4,-4,-4,-4,-4
max: 5,11,11,11,11,13,21,21,22
所以最大連續子段和爲22。事實上22=4+9+8-6+7.
好了,介紹完了連續最大子段和之後,就可以用它來解決我們前面所提出的相差最小的兩個元素之差這個題了。
b[1..n]爲所給的原始數值,
構造如下a[1..n-1],使得a[i]=b[i]-b[i+1]; 於是,原始數據中
b[i]-b[j]=(b[i]-b[i+1])+ (b[i+1]-b[i+2])+…+(b[j-1]-b[j])=a[i]+a[i+1]+…+a[j];
問題的答案就出來了,b[1..n]的差問題,就轉化爲a[1..n-1]的連續子區間求和問題。
亦即:b[1..n]中兩個元素之差,等於a[1..n-1]中連續區間的和。解最小差值的問題,變成了解連續區間的最小和的問題,解法與上述算法四(貪心算法)類似。
下面舉例求解該問題:
b[1..6]:5,38,90,65,33,75
顯而易見,差值最小的是38-33=5;過程如下:
a[1..5]:-33,-52,25,32,-42
s[1..5]:-33,-85,60,92,50
執行前述貪心過程,注意現在是求最小和,而不是最大和了:
max_s=0
循環5次,過程中的變化:
max_s:0,0,60,92,92
min: -33,-85,-85,-85,-85
結果不正確!這是因爲負數的存在,所以-85這個結果是由5-95=-85得來的,上述求解過程確實求得了兩數的最小差,最小差爲-85,且一定是前一個數減後一個數。這與原題是不相符合的,所以應該對算法進行調整,使得輸出是差的絕對最小。
改進如下:
和前面一樣,b[1..n]得到a[1..n-1]和s[1..n-1];
b[1..6]:5,38,90,65,33,75
a[1..5]:-33,-52,25,32,-42
s[1..5]:-33,-85,60,92,50
我們把原先“求a[1..n-1]中連續區間的最小子段和”問題,改爲“求a[1..n-1]中連續區間的子段和的絕對值最小”
分兩種情況,如果是負數,那麼應該求最大的負數;如果是正數,那麼應該求最小的正數。所以,除了要記錄max_s之外,還要記錄min_s;原先的min,應方該改爲min_positive和max_minus;僞代碼如下:
????真的很不好意思了,分析到這一步,發現會有很多問題。感覺不能用最大子段和這種類似的方法。因爲無法用記錄最小和min_s和最大和max_s這樣的方法去得到絕對值。
如果問題作如下的修正,是可以做出來的:
“請在線性時間,線性空間的條件下求一個未排序數組的最大差值”這樣可以用前述的貪心算法來分別求最小值(可能爲負)和最大值,比較這兩個值的絕對值,以求出絕值較大的那個。但是這樣就沒意思了,因爲這樣的題目只要找出最小值和最大值就可以了。
希望有高手能給出此題的高效解法。