利用分治策略來解決最大子數組問題

求解一個數組的最大子數組,例如給定數組B[0,,,,,,n-1],就是找到滿足條件的一組i和j使得當i <= j時,有B[j] - B[i]的值達到最大。這裏我們很容易想到一種暴力求解的方法。從n個下標中任意選取兩個,就是n*(n-1)/2中組合,來計算得到最大子數組即可。此時這種算法的時間複雜度爲O(n^2),但是若我們採用如下分治策略來進行優化的話,我們可以得到一個算法時間複雜度爲O(nlogn)的算法。

分治策略:首先我們來對問題進行一下轉換,把求B的最大子數組的問題轉換成求A的最大子數組問題,數組A即反應B中數的變化的一個數組,其中一個元素A[i](i >=1)代表的是B[i]-B[i-1],這樣的話,我們看問題的角度就發生了一定的變化,即不在關心B中的每個數是多少,而是關心B中每個數相對於前一個數是如何變化的,即問題現在轉化成了尋找A的和最大的非空連續子數組。

在明確了問題轉化成爲求解A的和最大的非空連續子數組後,我們來思考如何用分治技術來求解最大子數組問題。假定我們尋找子數組A[low.......high]的最大子數組。使用分治技術意味着我們要將子數組劃分爲兩個規模儘量相等的子數組。也就是說,找到子數組的中央位置,比如mid,然後考慮求解兩個子數組A[low....mid]和A[mid+1...high]。經分析可知,A的任何連續子數組A[i....j]所處的位置必然是以下三種情況之一:

  1. 完全位於子數組A[low....mid]中,因此low <= i <= j <= mid
  2. 完全位於子數組A[mid+1....high]中,因此mid+1 <= i <= j <= high
  3. 跨越了中點,因此low <= i  <= j <= high
      因此A[low...high]的一個最大子數組所處的位置必然是三種情況之一。實際上A[low....high]的一個最大子數組必然是完全位於A[low...mid]中,完全位於A[mid+1,high]中,或者跨越終點的所有子數組的最大者。我們可以遞歸地求解A[low...mid]和A[mid+1...high]的最大子數組,因爲這兩個子數組問題仍然是最大子數組問題,只是規模更小。因此剩下的工作就是尋找跨越重點的最大子數組,然後在三者情況中選取和最大者。
      可以很容易地想到在線性時間內求出跨越中點的最大子數組。此問題並非原問題規模更小的實例,因爲它加入了限制——求出的子數組必須橫跨中點,任何跨越中點的子數組都由兩個子數組A[i...mid]和A[mid+1...j]組成,其中low  <= i <= mid 且 mid+1 <= j <= high。因此我們只需找出形如A[i...mid]和A[mid+1....j]的最大子數組,然後將其合併即可。
      在有了以上方法後,我們很容易來列出求解這個問題的最終算法:
  1. 若low == high 返回low,high,A[low]或者A[high]
  2. 計算mid = (low+high)/2
  3. 計算A(low,mid)的最大子數組
  4. 計算A(mid+1,high)的最大子數組
  5. 計算橫跨中點的A(low,mid,high)的最大子數組
  6. 選取一個最大的即可
      在有了這個算法後,代碼編寫就比較容易了,一種c++的實現如下
      
#include<iostream>
#include<climits>
using namespace std;

//定義子數組結構體,分別代表子數組的下界和上界,以及和
struct sub_array {
	int max_left;
	int max_right;
	int max_sum;
};
//尋找跨越中點的最大子數組的函數
sub_array Find_Max_Crossing_Subarray(int *A, int low, int mid, int high) {
	int left_sum = INT_MIN;
	int right_sum = INT_MIN;
	int sum = 0;
	sub_array cross_array;
	//計算中點左側的最大子數組
	for (int i = mid; i >= 1; i--) {
		sum = sum + A[i];
		if (sum > left_sum) {
			left_sum = sum;
			cross_array.max_left = i;
		}
	}
	sum = 0;
	//計算中點右側的最大子數組
	for (int j = mid + 1; j <= high; j++) {
		sum = sum + A[j];
		if (sum > right_sum) {
			right_sum = sum;
			cross_array.max_right = j;
		}
	}
	//將二者進行合併
	cross_array.max_sum = left_sum + right_sum;
	return cross_array;
}
//尋找最大子數組的函數
sub_array Find_Maximun_Subarray(int *A, int low, int high) {
    int mid;
	if (high == low) {     //數組中只有一個元素的情況
		sub_array temp;
		temp.max_left = low;
		temp.max_right = high;
		temp.max_sum = A[low];
		return temp;
	}
	else {
		mid = (low + high) / 2;
		sub_array cross_array;
		sub_array left_array;
		sub_array right_array;
		left_array = Find_Maximun_Subarray(A, low, mid);    //計算左子數組
		right_array = Find_Maximun_Subarray(A, mid + 1, high);    //計算右子數組
		cross_array = Find_Max_Crossing_Subarray(A, low, mid, high);    //計算橫跨中點的子數組
		//尋找最大的情況
		if (left_array.max_sum >= right_array.max_sum && left_array.max_sum >= cross_array.max_sum) {
			return left_array;
		}
		else if (right_array.max_sum >= left_array.max_sum && right_array.max_sum >= cross_array.max_sum) {
			return right_array;
		}
		else {
			return cross_array;
		}
	}
}
int main() {
	int num;
	cin >> num;
	int *B = new int[num];
	int *A = new int[num];
	for (int i = 0; i < num; i++) {
		cin >> B[i];
	}
	for (int i = 1; i < num; i++) {
		A[i] = B[i] - B[i-1];
	}
	sub_array solution;
	solution = Find_Maximun_Subarray(A, 1, num-1);
	cout << solution.max_left<<" "<< solution.max_right<<" "<<solution.max_sum << endl;
	system("pause");
}

算法分析:可以看出當n = 1時,算法的時間複雜度爲O(1),當n >= 2是,T(n) = 2T(n/2) + O(n),故綜上所述,算法的時間複雜度爲O(nlogn),可以看出比起暴力求解的O(n^2)的時間複雜度,已經明顯優化了不少。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章