1. 程式人生 > >演算法導論-分治、最大子序列問題

演算法導論-分治、最大子序列問題

一.基本概念
分治法的基本步驟:
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

執行流程:從頭到尾遍歷目標陣列,將陣列分割為滿足上述條件的子串,同時得到各子串的最大字首和,然後比較各子串的最大字首和,得到最終答案。

以array={-2, 1, -3, 4, -1, 2, 1, -5, 4}為例,來簡單說明一下演算法步驟。通過遍歷,可以將陣列分割為如下3個子串(-2),(1,-3),(4,-1,2,1,-5,4),
這裡對於(-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;
}

執行結果: