演算法導論-分治、最大子序列問題
阿新 • • 發佈:2019-02-03
一.基本概念
分治法的基本步驟:
1.分解問題(Divide):把原問題分解為若干個與原問題性質相類似的子問題;
2.求解字問題(Conquer):不斷分解子問題並求解;
3.合併子問題的解(Combine).
分治法的運用條件:
1.原問題可以分解為若干與原問題的解;
2.子問題可以分解並可以求解;
3.子問題的解可以合併為原問題的解;
4.分解後的子問題應互相獨立,即不包含重疊子問題(如菲不那切豎列)。
求解遞迴函式的方法:
1.代換法
1)猜測解的行為;
2)數學歸納法證明。
2.遞迴樹法
在遞迴樹中,每一個結點都代表遞迴函式呼叫集合中一個子問題的代價。將樹中每一層內的代價相加得到一個每層代價的集合,再將每層的代價相加得到遞迴是所有層次的總代價。
3.主方法
二.最大子序列問題
對於一個包含負值的數字串array[1...n],要找到他的一個子串array[i...j](0<=i<=j<=n),使得在array的所有子串中,array[i...j]的和最大。
這裡我們需要注意子串和子序列之間的區別。子串是指陣列中連續的若干個元素,而子序列只要求各元素的順序與其在陣列中一致,而沒有連續的要求。
1.暴力解法,時間複雜度O(n~2)。
i表示子序列起始下標,j表示內部迴圈開始下表,遍歷子序列的開頭和結束下標,計運算元序列的和,
2.Kadane演算法,時間複雜度(0(n)).
原理:將陣列從左到右分割為若干子串,使得除了最後一個子串之外,其餘子串的各元素之和小於0,
且對於所有子串array[i...j]和任意k(i<=k<j),有array[i...k]的和大於0。滿足條件的和最大子串,
這裡對於(-2)這樣的情況,單獨分為一組。各子串的最大字首和為-2,1,6,所以目標串的最大子串和為6。
3.遞迴實現,時間複雜度為O(nlogn)。
本期的主角上場。
main函式呼叫:
執行結果:
分治法的基本步驟:
1.分解問題(Divide):把原問題分解為若干個與原問題性質相類似的子問題;
2.求解字問題(Conquer):不斷分解子問題並求解;
3.合併子問題的解(Combine).
分治法的運用條件:
1.原問題可以分解為若干與原問題的解;
2.子問題可以分解並可以求解;
3.子問題的解可以合併為原問題的解;
4.分解後的子問題應互相獨立,即不包含重疊子問題(如菲不那切豎列)。
求解遞迴函式的方法:
1.代換法
1)猜測解的行為;
2)數學歸納法證明。
2.遞迴樹法
在遞迴樹中,每一個結點都代表遞迴函式呼叫集合中一個子問題的代價。將樹中每一層內的代價相加得到一個每層代價的集合,再將每層的代價相加得到遞迴是所有層次的總代價。
3.主方法
主要是記憶三種情況,根據各種情況直接寫出遞迴函式。
T(n) = aT(n/b)+h(n)
a >=1 ; b >1 ; h(n) : 不參與遞迴的複雜度函式
判斷n^log b (a)與h(n)的大小關係
= Θ(h(n)) :該方法的複雜度為 Θ(h(n)*lg(n))
> Θ(h(n)) :該方法的複雜度為 Θ(n^(log a/log b))
< Θ(h(n)) :該方法複雜度為 Θ(h(n))
這樣可以幫助你快速的分析出你得演算法的複雜度是否符合要求。
二.最大子序列問題
對於一個包含負值的數字串array[1...n],要找到他的一個子串array[i...j](0<=i<=j<=n),使得在array的所有子串中,array[i...j]的和最大。
這裡我們需要注意子串和子序列之間的區別。子串是指陣列中連續的若干個元素,而子序列只要求各元素的順序與其在陣列中一致,而沒有連續的要求。
1.暴力解法,時間複雜度O(n~2)。
i表示子序列起始下標,j表示內部迴圈開始下表,遍歷子序列的開頭和結束下標,計運算元序列的和,
int MaxSubSum (int a[], int n)
{
int i, j, maxSum = 0;
for (i = 0; i < n; i++)
{
int thisSum = 0;
for (j = i; j < n; j++)
{
thisSum += a[j];
if (thisSum > maxSum)
maxSum = thisSum;
}
}
return maxSum;
}
2.Kadane演算法,時間複雜度(0(n)).
原理:將陣列從左到右分割為若干子串,使得除了最後一個子串之外,其餘子串的各元素之和小於0,
且對於所有子串array[i...j]和任意k(i<=k<j),有array[i...k]的和大於0。滿足條件的和最大子串,
只能是上述某個子串的字首,而不可能跨越多個子串。
原理詳細可參考:http://blog.csdn.net/joylnwang/article/details/6859677
執行流程:從頭到尾遍歷目標陣列,將陣列分割為滿足上述條件的子串,同時得到各子串的最大字首和,然後比較各子串的最大字首和,得到最終答案。
這裡對於(-2)這樣的情況,單獨分為一組。各子串的最大字首和為-2,1,6,所以目標串的最大子串和為6。
int KadaneMax (int a[], int n)
{
int i, j, curMax, max, curLeft, curRight;
curMax = max = curLeft = curRight = 0;
for (i = 0; i < n; i++)
{
curMax += a[i];
if (curMax > 0)
{
curRight = i;
//更新最大值
if (max < curMax)
max = curMax;
}
//重新分割子串
else
{
curMax = 0;
curLeft = curRight = i + 1;
}
}
return max;
}
3.遞迴實現,時間複雜度為O(nlogn)。
本期的主角上場。
分治的思想:最大子序列和可能出現在三處。或者整個出現在輸入資料的左半部,或者整個出現右半部,或者跨越輸入資料的中部從而佔據左右兩個半部分。前兩種情況遞迴求解。第三種情況的最大和可以通過求出前半部分的最大和(包含前半部分的最後一個元素)以及後半部分的最大和(包含後半部分的第一個元素)而得到,然後將這兩個和加在一起,求出三個值的最大值。
int recursionMax (int a[],int left, int right)
{
int i,j;
if (left == right) //base case
if (a[left] > 0)
return a[left];
else
return 0;
int center = (left + right) / 2;
// //每次遞迴返回時,該值為該子段的最終左最大子序列和
int maxLeftSum = recursionMax (a, left, center);
//每次遞迴返回時,該值為該子段的右最大自序列和
int maxRightSum = recursionMax(a, center + 1, right);
//從中間向左擴充套件求子段的最大子序列和,必然包括子段的最右端數
int maxLeftBorderSum = 0, leftBorderSum = 0;
for (i = center; i >= left; i--)
{
leftBorderSum += a[i];
if (leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
int maxRightBorderSum = 0, rightBorderSum = 0;
for (j = center + 1; j <right; j++)
{
rightBorderSum += a[j];
if (rightBorderSum > maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
int tmp=maxLeftSum>maxRightSum?maxLeftSum:maxRightSum;
if (tmp>=maxLeftBorderSum + maxRightBorderSum) return tmp;
else return maxLeftBorderSum + maxRightBorderSum;
}
main函式呼叫:
int main ()
{
int a[] = { -2, 1, -3, 4, -1, 2, 1, -5, 4,0};
printf ("MaxSubSum is:%6d.\n", MaxSubSum(a, 9));
printf ("KadaneMax is:%6d.\n", KadaneMax(a, 9));
printf ("recursionMax is:%6d.\n", recursionMax(a,0, 9));
return 0;
}
執行結果: