標籤:遞迴 資料結構 演算法 最大子序列和
我的主力部落格:半畝方塘
對於 《資料結構與演算法分析——C語言描述》 一書第 20 頁所描述的演算法 3,相信會有很多人表示不怎麼理解,下面我由具體問題的求解過程出發,談談我自己的理解:
首先,什麼是分治法呢?所謂 分治法,就是 將一個問題的求解過程分解為兩個大小相等的子問題進行求解,如果分解後的子問題本身也可以分解的話,則將這個分解的過程進行下去,直至最後得到的子問題不能再分解為止,最後將子問題的解逐步合并並可能做一些少量的附加工作,得到最後整個問題的解。在求解原來整個問題的演算法思想,與求解每一個子問題的演算法思想完全相同,則可以用到遞迴來解決這個問題,在我的博文 關於遞迴的一些簡單想法 中,曾指出,當我們要解決的問題有著 反覆執行的基本操作 的時候,可以考慮使用遞迴,在這裡,原來的整個的問題與每一個分解後子問題都有著反覆執行的演算法思想,這就是一個基本操作,所以可以用遞迴實現,關於遞迴,在我的博文 由遞迴思想處理問題的基本原則 中,給出了有關遞迴思想的部分描述。
回到我們標題所闡述的問題,求最大子序列和,我們可以將求最大子序列和的序列分解為兩個大小相等的子序列,然後在這兩個大小相等的子序列中,分別求最大子序列和,如果由原序列分解的這兩個子序列還可以進行分解的話,進一步分解,直到不能進行分解為止,使問題逐步簡化,最後求最簡化的序列的最大子序列和,沿著分解路徑逐步回退,合成為最初問題的解。我們知道,最大子序列和只可能在三個位置求出:
- 序列的左半部分的最大子序列和
- 序列的右半部分的最大子序列和
- 橫跨序列左半部分和右半部分得到的最大子序列和:對包含左半部分的最後一個元素的最大子序列和以及包含右半部分第一個元素的最大子序列和二者求和所得到的值
- 比較三者的大小,最大者即為所求的最大子序列和
下面我們通過具體的執行個體來仔細體會一下這種 分治 的演算法思想。
如果我們要求下面序列的最大子序列:
4 -3 5 -2 -1 2 6 -2
將這個子序列存放在一個數組中來考慮,則有 int a[8] = {4, -3, 5, -2, -1, 2, 6, -2}
。
按照分治法的思想,首先將這個序列分為左右兩半部分,分界點 是 序列首元素在數組中的下標和尾元素在數組中的下標的和除以 2 所得到的下標值。在上面的序列中,分界點就是 (0 + 7)/2 = 3,也就是說分界點是下標為 3 的元素,即 -2,按照這個分界點,將序列分為兩半部分,左半部分子序列為:
4 -3 5 -2
右半部分子序列為:
-1 2 6 -2
我們要在分解後所形成的兩個子序列中,分別求最大子序列和,我們不妨用左半部分的子序列來分析一下:
4 -3 5 -2
求這個左半部分子序列的最大子序列和,我們還可以將這個左半部分子序列按照上面提到的方法分解為左半部分和右半部分,由上面的分解方法,得到分界點為下標是 1 的元素,即 -3,由此我們得到左半部分的子序列為:
4 -3
右半部分的子序列為:
5 -2
上面得到的左半部分子序列和右半部分子序列要分別求最大子序列和,同樣,這兩個子序列仍然可以分解為左半部分和右半部分,針對上面得到的左半部分的子序列,由上面的分解方法,這裡省略分解過程,得到最後的左半部分子序列為:
4
右半部分子序列為:
-3
針對 5 -2 ,得到左半部分的子序列為:
5
右半部分的子序列為:
-2
針對上面分解所得到的子序列,每一個子序列只含有一個元素,這是子序列的最簡情形,即首元素在數組中的下標和尾元素在數組中的下標相同(首元素和尾元素為同一元素),此時序列不能再進行分解了( 這種情況將得到遞迴的基準情形 )。
考慮上面最後得到的不能分解的子序列,按照最先提到的求最大子序列和的演算法思想(1.2.3.4.),可以得到如下結論:
顯然,針對序列 4 -3,左半部分子序列的最大子序列和是 4(是左半部分子序列本身);右半部分子序列的最大子序列和是 -3(是右半部分子序列本身);左半部分子序列中包含最後一個元素 4 的最大子序列和為 4,右半部分子序列中包含第一個元素 -3 的最大子序列和為 -3,二者求和得到橫跨左半部分和右半部分的最大子序列和是 4 + (-3) = 1;在這三者中,左半部分的最大子序列和 4 是最大的,由此得到序列 4 -3 中,最大子序列和是 4。同理,針對序列 5 -2,我們可以用同樣的方法得到最大子序列和為 5。
而序列 4 -3 和序列 5 -2 又分別是序列 4 -3 5 -2 的左半部分子序列和右半部分子序列,由此我們得到了序列 4 -3 5 -2 的左半部分子序列的最大子序列和為 4;右半部分的最大子序列和為 5;左半部分子序列中,包含最後一個元素 -3 的最大子序列和是 -3 + 4 = 1,右半部分子序列中,包含第一個元素 5 的最大子序列和為 5,二者求和得到橫跨左半部分和右半部分的最大子序列和為 1 + 5 = 6,三者中 6 是最大的,由此,我們得到序列 4 -3 5 -2 的最大子序列和為 6。而序列 4 -3 5 -2 恰好是原序列的左半部分子序列,依照上述求原序列左半部分最大子序列和的方法,同理我們可以很輕鬆地求出原序列右半部分子序列 -1 2 6 -2 的最大子序列和為 8(不妨在草稿紙上示範一下這個過程),經過以上分析過程,我們得到:
原序列的左半部分子序列的最大子序列和是 6;原序列的右半部分子序列的最大子序列和為 8;在原序列的左半部分子序列中,包含最後一個元素 -2 的最大子序列和是 -2 + 5 + (-3) + 4 = 4,在原序列的右半部分子序列中,包含第一個元素 -1 的最大子序列和是 -1 + 2 + 6 = 7,二者求和得到橫跨左半部分與右半部分的最大子序列和是 4 + 7 = 11, 6 8 11 中最大的為 11,由此我們可以得到原序列的最大子序列和為 11。
由以上分析可以看到,求一個序列的最大子序列和,是按照分治法的思想將所給序列逐步分解,分解到不能分解為止(即遞迴的基準情形),然後再逐步回退,分別求各個分解的子序列的最大子序列和,最後將所有的結果合成在一起得到最後的結果,這裡涉及到一個 反覆進行的基本操作 ,就是 分別求各個分解的子序列的最大子序列和 。
經過對以上個例的分析,我相信可以更好地理解下面由分治法和遞迴思想相結合的求最大子序列和的代碼了:
static int MaxSubSum(const int A[], int Left, int Right){ if (Left == Right) /* 遞迴的基準情形 */ return a[Left]; int Center; Center = (Left + Right) / 2; /* 求分界點 */ int MaxLeftSum; MaxLeftSum = MaxSubSum(A, Left, Center); /* 遞迴,求左半部分子序列的最大子序列和 */ int MaxRightSum; MaxRightSum = MaxSubSum(A, Center + 1, Right); /* 遞迴,求右半部分子序列的最大子序列和 */ /* 求橫跨左半部分和右半部分的最大子序列和 */ /* 首先是左半部分子序列中包含最後一個元素的最大子序列和 */ int MaxLeftBorderSum = A[Center], LeftBorderSum = A[Center]; for (int i = Center - 1; i >= Left; --i) { LeftBorderSum += A[i]; if (LeftBorderSum > MaxLeftBorderSum) MaxLeftBorderSum = LeftBorderSum; } /* 接著是右半部分子序列中包含第一個元素的最大子序列和 */ int MaxRightBorderSum = A[Center + 1], RightBorderSum = A[Center + 1]; for (int i = Center + 2; i <= Right; ++i) { RightBorderSum += A[i]; if (RightBorderSum > MaxRightBorderSum) MaxRightBorderSum = RightBorderSum; } /* Max3 返回左、右半部分子序列的最大子序列和以及橫跨左、右半部分的最大子序列和中的最大者 */ return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum);}int MaxSubsequenceSum(const int A[], int N) /* 求最大子序列和 */{ return MaxSubSum(A, 0, N - 1);}