最大子數組問題
最大子數組問題
本文只是做一個記錄,更細致的思路請查看算法導論
最大子數組結構體
typedef struct {
int low, high, sum;
} SubArray;
暴力求解
計算所有的數組區間的和進而得到最大的子數組,算法復雜度為θ(n2)。這種方法在小規模的數據表現很好,d但是在大規模數據中則很糟糕,但可用作分治算法的改進。實現的思路是先計算從以i
為起始的最大子數組,然後從[1..n],[2..n] .. [n..n]中找到最大的子數組。
SubArray MaxSubArray_BruteForce(int low, int high, int A[]) { int sum; int max_subarray = 0; SubArray *S = (SubArray *)calloc((high - low + 2), sizeof(SubArray)); for (int i = low, k = 0; i <= high; ++i, ++k) { S[k].sum = INT_MIN; S[k].low = i; sum = 0; for (int j = i; j <= high; ++j) { // 計算從以`i`為起始的最大子數組 sum += A[j]; if (S[k].sum < sum) { S[k].sum = sum; S[k].high = j; } } if (S[max_subarray].sum <= S[k].sum) // = 是出現 {0,0,1}的情況精確的計算子數組為[2,2]{1} max_subarray = i - low; } return S[max_subarray]; }
分治算法
從分治算法的時間分析來看,
$$
T(n)=\begin{cases}
\theta(1) & if & n = 1 \
a\theta(n/a) + \theta(n) & if & n > 1
\end{cases}
$$
如果要尋找[low..high]的最大子數組,使用分治的技術意味我們要將數組分為規模盡可能相等的兩個子數組(上a為2),尋找出中間的位置mid,再考慮求解子數組[low..mid]和[mid+1..high]。數組[low..high]的最大子數組[i..j]必然是下面三種情況:
- 完全位於子數組[low..mid]中,因此
low <= i <= high <= mid
- 完全位於子數組[mid+1..high]中,因此
mid+1 <= i <= j <= high
- 跨域了中點mid,因此位於
low <= i <= mid <= j <= high
且規定跨越中點的數組 必須包含mid 和 mid + 1
遞歸的求解子數組[low..mid]和[mid+1..high],減小子問題問題的規模,最後尋找跨越中點的最大子數組,與歸並排序的歸並過程相似,我們把尋找跨越中點的最大子數組作為子問題的合並過程。簡單的邏輯為
- 尋找子數組[low..mid]的最大子數組
- 尋找子數組[mid+1..high]的最大子數組
- 尋找[low..mid mid+1..high]的最大子數組
- 返回三個最大子數組中的子數組。註:這裏的比較必須要用加上等號,這樣可以跳過0更精確的求到最大子數組
SubArray MaxCrossSubArray(int low, int mid, int high, int A[]) {
int left_sum, right_sum, sum;
SubArray S;
left_sum = right_sum = INT_MIN;
sum = 0;
for (int i = mid; i >= low; i--) {
sum += A[i];
if (left_sum < sum) {
S.low = i;
left_sum = sum;
}
}
sum = 0;
for (int j = mid + 1; j <= high; j++) {
sum += A[j];
if (right_sum < sum) {
S.high = j;
right_sum = sum;
}
}
S.sum = left_sum + right_sum;
return S;
}
SubArray MaxSubArray_DivideConquer(int low, int high, int A[]) {
if (low == high) {
SubArray S;
S.low = low;
S.high = high;
S.sum = A[low];
return S;
}
int mid = (low + high) / 2;
SubArray L = MaxSubArray_DivideConquer(low, mid, A);
SubArray R = MaxSubArray_DivideConquer(mid + 1, high, A);
SubArray M = MaxCrossSubArray(low, mid, high, A);
return Max3(L, R, M);
}
改進後的遞歸算法
前面提到暴力求解雖然在大規模數據表現非常差,但是在小規模的求解時優勢很大。當子問題的規模小於某個值n
時,我們用暴力算法求解
// 暴力算法和分治算法在 40 左右達到性能交叉點
// 在規模在 10 左右,暴力算法大幅領先分治算法
SubArray MaxSubArray_Synergy(int low, int high, int A[]) {
if (high - low < 10)
return MaxSubArray_BruteForce(low, high, A);
int mid = (low + high) / 2;
SubArray L = MaxSubArray_Synergy(low, mid, A);
SubArray R = MaxSubArray_Synergy(mid + 1, high, A);
SubArray M = MaxCrossSubArray(low, mid, high, A);
return Max3(L, R, M);
}
線性算法
已知[1..j]的最大子數組,計算[1..j+1]最大子數組的思路:[1..j+1]的最大子數組要麽是[1..j]的最大子數組,要麽是某個子數組[i..j+1] (1 <= i <= j+1 )。具體實現如註釋所述
SubArray MaxSubArray_Linear(int low, int high, int A[]) {
SubArray S = {-1, -1, INT_MIN};
int sum = 0;
int slow = -1;
for (int i = low; i <= high; ++i) {
if (sum > 0) { // 加上A[i]後當前最大子數組為正,中間的非負數項繼續保留
sum += A[i];
} else { // 重新尋找最大子數組
sum = A[i];
slow = i;
}
if (sum > S.sum) { // 新的最大子數組大於舊最大子數組
S.low = slow;
S.high = i;
S.sum = sum;
}
}
return S;
}
寫代碼時碰到的一些問題
- 剛開始我的遞歸實現是還有一個SubArray指針的,但是在內存裏面真正的SubArray只有一份,每一次計算SubArray變量都在改變。其實我們只需要子數組的下標與和,所以直接在函數內定義等到函數結束棧內存回收也沒有關系,因為已經返回。
- 暴力算法剛開始的i都是從0開始,如果只是單純的調用不會產生問題,但是當遞歸算法小規模取調用的情況下就會出現段錯誤(越界了)
- 最大子數組大小一樣但是幾種算法的下標不一樣,出現這種的問題是沒有把0過濾,如在線性算法中 應該為 if (sum > 0) 而不應該為 if (sum >= 0),遞歸算法中的Max3比較最大子數組的問題則應該加上等號判斷。還有一種是出現這種情況{1, 1, -2, 1, 1}, 這幾種算法出現了兩種答案[0-1]和[3-4],這個問題目前還沒有解決
四種算法的時間復雜度:
$$
\begin{cases}
BruteForce & \theta(n^2) & \
DivideConquer & \theta(nlog(n)) \
BruteForce+DivideConquer & \theta(nlog(n)) \
Linear & \theta(n)
\end{cases}
$$
處理10w(為什麽不是更大的數,因為暴力算法太慢了),四種算法的時間大概是
- 暴力算法 15.12s
- 簡單遞歸算法 0.15s
- 優化後的遞歸算法 0.12s
- 線性算法忽略不計 0.003s
最大子數組問題