1. 程式人生 > >Algorithm —— 最大子陣列求解(五)

Algorithm —— 最大子陣列求解(五)

Algorithm —— 最大子陣列求解

《演算法導論》中引出了一個求某個陣列的和最大子陣列問題:在原陣列A中,求一個子陣列a,它的各元素值的和是A的各個子陣列中最大的;且子陣列a的各元素下標值要連續。

要注意的是,如果要求某個陣列的最大子陣列,則此陣列中的值必須要包含負值;否則,求最大子陣列是沒有意義的,因為此時,整個陣列的和肯定是最大的,就不必再求了。

我們使用分治策略來解決該問題。假定我們要尋找陣列A[low...high]的最大子陣列,因為要使用分治技術,這也就意味著我們要將陣列A劃分為兩個規模儘量相等的子陣列。也就是找到陣列A的中央位置mid,然後考慮求解兩個子陣列A[low...mid]和A[mid+1...high]。

我們可以知道,陣列A[low...high]的任何連續子陣列A[i...j]所處的位置必然是一下三種情況之一:

  1. 完全位於子陣列A[low...mid]中,因此low =< i =< j =< mid;子陣列完全位於A[low...mid]中。
  2. 完全位於子陣列A[mid+1...high]中,因此mid < i =<j =< high;子陣列完全位於A[mid+1...high]中。
  3. 跨越了中點mid,因此lwo =< i =< mid < j =< high;子陣列跨越了中點mid。


因此,A[low...high]的一個最大子陣列所處的位置必然是上述三種情況之一。對於前兩種情況,我們可以遞迴求解,因為 這兩個問題仍是最大子陣列問題,只是規模更小;剩下的工作就是尋找跨越中點的最大子陣列。所以問題的最終結果就是在三種情況中選取和最大者。

尋找跨越中點的最大子陣列問題並不是原問題規模更小的例項,因為它加入了限制——求出的子陣列必須跨越中點。任何跨越中點的子陣列都由兩個子陣列A[i...mid]和A[mid+1...j]組成,其中low =< i =< mid < j =< high。因此,我們只需找出形如A[i...mid]和A[mid+1...j]的最大子陣列,然後將其合併即可。此問題的函式如下所示:

	/**
	 * 
	 * @author coder
	 * 
	 *         代表'和最大子陣列'資訊的封裝型別
	 * 
	 */
	private class MaxSumSubArray {

		public MaxSumSubArray(int left_index, int right_index, int sum) {
			this.left_index = left_index;
			this.right_index = right_index;
			this.sum = sum;
		}

		int left_index;// 陣列的左下標邊界值
		int right_index;// 陣列的右下標邊界值
		int sum;// 此子陣列各元素的和值

		@Override
		public String toString() {
			return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
		}
	}

	/**
	 * 返回一個跨越中點的'和最大子陣列'的邊界,及此陣列中所有值的和;因為要尋找的'和最大子陣列'下標必須要跨越中點,所以該子陣列的下標範圍一定是從中點向左右兩邊延伸、且連續;
	 * 
	 * 對於左陣列,我們只需在下標範圍[low...mid]中,從mid值開始由右向左依次計算各元素相加的和,並逐一比較各次所得和的大小;記錄當前得到的最大的和與此時運算的陣列下標值i;迴圈結束後,就得到了這個和最大子陣列的左半部分a[i...mid]
	 * 對於右陣列,也是類似,從(mid+1)開始,從左向右遍歷計算各元素相加的和;得到當前最大的和值時的下標j;迴圈結束後,就得到這個子陣列的右半部分b[mid+1...high]
	 * 
	 * 合併子陣列a/b,就得到了當前跨越中點的'和最大子陣列'c[i...j];
	 * 
	 * @param array
	 *            當前需要處理的陣列
	 * @param low
	 *            在當前陣列中需要處理的子陣列的下標左邊界
	 * @param mid
	 *            在當前數子組中需要處理的下標範圍對應的下標中點
	 * @param heigh
	 *            在當前陣列中需要處理的子陣列的下標右邊界
	 * @return 返回一個MaxSumSubArray例項,即 跨越中點的'和最大子陣列'的封裝物件
	 */
	private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {

		int left_sum = 0;// 記錄每次左邊找到的最大和值
		int sum = 0; // 記錄左邊子陣列所有元素的和值
		int max_left = -1;// 記錄每次找到左邊子陣列最大和值時的下標值

		for (int i = mid; i >= low; i--) {
			sum = sum + array[i];
			if (sum > left_sum) {
				left_sum = sum;
				max_left = i;
			}
		}

		int right_sum = 0;// 記錄每次右邊找到的最大和值
		sum = 0;//// 記錄右邊子陣列所有元素的和值
		int max_right = -1;// 記錄每次找到右邊子陣列最大和值時的下標值

		for (int j = mid + 1; j <= high; j++) {
			sum = sum + array[j];
			if (sum > right_sum) {
				right_sum = sum;
				max_right = j;
			}
		}

		MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);

		return obj;

	}
此時,我們就解出了求跨越中點的最大子陣列問題;又由於前兩種情況是原問題規模更小的例項,可用遞迴求解;所以此時求陣列A[low...high]的最大子陣列問題的解如下:
	/**
	 * 
	 * @author coder
	 * 
	 *         代表'和最大子陣列'資訊的封裝型別
	 * 
	 */
	private class MaxSumSubArray {

		public MaxSumSubArray(int left_index, int right_index, int sum) {
			this.left_index = left_index;
			this.right_index = right_index;
			this.sum = sum;
		}

		int left_index;// 陣列的左下標邊界值
		int right_index;// 陣列的右下標邊界值
		int sum;// 此子陣列各元素的和值

		@Override
		public String toString() {
			return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
		}
	}

	/**
	 * 找到目標陣列array的一個'和最大子陣列',並返回該'和最大子陣列'的下標及所有元素和資訊
	 * 
	 * @param array
	 *            目標源陣列
	 * @param low
	 *            需要操作的子陣列的下標左邊界
	 * @param high
	 *            需要操作的子陣列的下標右邊界
	 * @return 返回從目標值陣列中指定的子陣列c[low...high]的'和最大子陣列'資訊
	 */
	public MaxSumSubArray findMaxImumSubArray(int[] array, int low, int high) {

		if (low > high || array == null) {
			throw new IllegalArgumentException("findMaxImumSubArray Argument is illegal!");
		}

		if (high == low) {
			return new MaxSumSubArray(low, high, array[low]);
		}

		int mid = (low + high) / 2;

		MaxSumSubArray leftMaxSSArray = findMaxImumSubArray(array, low, mid);
		MaxSumSubArray rightMaxSSArray = findMaxImumSubArray(array, mid + 1, high);
		MaxSumSubArray crossMidMaxSSArray = findMaxCrossingSubArray(array, low, mid, high);

		if (leftMaxSSArray.sum >= rightMaxSSArray.sum && leftMaxSSArray.sum >= crossMidMaxSSArray.sum)
			return leftMaxSSArray;
		else if (rightMaxSSArray.sum >= leftMaxSSArray.sum && rightMaxSSArray.sum >= crossMidMaxSSArray.sum)
			return rightMaxSSArray;

		return crossMidMaxSSArray;
	}

	/**
	 * 返回一個跨越中點的'和最大子陣列'的邊界,及此陣列中所有值的和;因為要尋找的'和最大子陣列'下標必須要跨越中點,所以該子陣列的下標範圍一定是從中點向左右兩邊延伸、且連續;
	 * 
	 * 對於左陣列,我們只需在下標範圍[low...mid]中,從mid值開始由右向左依次計算各元素相加的和,並逐一比較各次所得和的大小;記錄當前得到的最大的和與此時運算的陣列下標值i;迴圈結束後,就得到了這個和最大子陣列的左半部分a[i...mid]
	 * 對於右陣列,也是類似,從(mid+1)開始,從左向右遍歷計算各元素相加的和;得到當前最大的和值時的下標j;迴圈結束後,就得到這個子陣列的右半部分b[mid+1...high]
	 * 
	 * 合併子陣列a/b,就得到了當前跨越中點的'和最大子陣列'c[i...j];
	 * 
	 * @param array
	 *            當前需要處理的陣列
	 * @param low
	 *            在當前陣列中需要處理的子陣列的下標左邊界
	 * @param mid
	 *            在當前數子組中需要處理的下標範圍對應的下標中點
	 * @param heigh
	 *            在當前陣列中需要處理的子陣列的下標右邊界
	 * @return 返回一個MaxSumSubArray例項,即 跨越中點的'和最大子陣列'的封裝物件
	 */
	private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {

		int left_sum = 0;// 記錄每次左邊找到的最大和值
		int sum = 0; // 記錄左邊子陣列所有元素的和值
		int max_left = -1;// 記錄每次找到左邊子陣列最大和值時的下標值

		for (int i = mid; i >= low; i--) {
			sum = sum + array[i];
			if (sum > left_sum) {
				left_sum = sum;
				max_left = i;
			}
		}

		int right_sum = 0;// 記錄每次右邊找到的最大和值
		sum = 0;//// 記錄右邊子陣列所有元素的和值
		int max_right = -1;// 記錄每次找到右邊子陣列最大和值時的下標值

		for (int j = mid + 1; j <= high; j++) {
			sum = sum + array[j];
			if (sum > right_sum) {
				right_sum = sum;
				max_right = j;
			}
		}

		MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);

		return obj;

	}
完整的測試程式碼如下:
public class AlgorithmTest {

	public static void main(String[] args) {
		int[] array = { 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7 };
		AlgorithmTest algorithm = new AlgorithmTest();
		MaxSumSubArray res = algorithm.findMaxImumSubArray(array, 0, array.length - 1);
		System.out.println(res);
	}

	/**
	 * 
	 * @author coder
	 * 
	 *         代表'和最大子陣列'資訊的封裝型別
	 * 
	 */
	private class MaxSumSubArray {

		public MaxSumSubArray(int left_index, int right_index, int sum) {
			this.left_index = left_index;
			this.right_index = right_index;
			this.sum = sum;
		}

		int left_index;// 陣列的左下標邊界值
		int right_index;// 陣列的右下標邊界值
		int sum;// 此子陣列各元素的和值

		@Override
		public String toString() {
			return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
		}
	}

	/**
	 * 找到目標陣列array的一個'和最大子陣列',並返回該'和最大子陣列'的下標及所有元素和資訊
	 * 
	 * @param array
	 *            目標源陣列
	 * @param low
	 *            需要操作的子陣列的下標左邊界
	 * @param high
	 *            需要操作的子陣列的下標右邊界
	 * @return 返回從目標值陣列中指定的子陣列c[low...high]的'和最大子陣列'資訊
	 */
	public MaxSumSubArray findMaxImumSubArray(int[] array, int low, int high) {

		if (low > high || array == null) {
			throw new IllegalArgumentException("findMaxImumSubArray Argument is illegal!");
		}

		if (high == low) {
			return new MaxSumSubArray(low, high, array[low]);
		}

		int mid = (low + high) / 2;

		MaxSumSubArray leftMaxSSArray = findMaxImumSubArray(array, low, mid);
		MaxSumSubArray rightMaxSSArray = findMaxImumSubArray(array, mid + 1, high);
		MaxSumSubArray crossMidMaxSSArray = findMaxCrossingSubArray(array, low, mid, high);

		if (leftMaxSSArray.sum >= rightMaxSSArray.sum && leftMaxSSArray.sum >= crossMidMaxSSArray.sum)
			return leftMaxSSArray;
		else if (rightMaxSSArray.sum >= leftMaxSSArray.sum && rightMaxSSArray.sum >= crossMidMaxSSArray.sum)
			return rightMaxSSArray;

		return crossMidMaxSSArray;
	}

	/**
	 * 返回一個跨越中點的'和最大子陣列'的邊界,及此陣列中所有值的和;因為要尋找的'和最大子陣列'下標必須要跨越中點,所以該子陣列的下標範圍一定是從中點向左右兩邊延伸、且連續;
	 * 
	 * 對於左陣列,我們只需在下標範圍[low...mid]中,從mid值開始由右向左依次計算各元素相加的和,並逐一比較各次所得和的大小;記錄當前得到的最大的和與此時運算的陣列下標值i;迴圈結束後,就得到了這個和最大子陣列的左半部分a[i...mid]
	 * 對於右陣列,也是類似,從(mid+1)開始,從左向右遍歷計算各元素相加的和;得到當前最大的和值時的下標j;迴圈結束後,就得到這個子陣列的右半部分b[mid+1...high]
	 * 
	 * 合併子陣列a/b,就得到了當前跨越中點的'和最大子陣列'c[i...j];
	 * 
	 * @param array
	 *            當前需要處理的陣列
	 * @param low
	 *            在當前陣列中需要處理的子陣列的下標左邊界
	 * @param mid
	 *            在當前數子組中需要處理的下標範圍對應的下標中點
	 * @param heigh
	 *            在當前陣列中需要處理的子陣列的下標右邊界
	 * @return 返回一個MaxSumSubArray例項,即 跨越中點的'和最大子陣列'的封裝物件
	 */
	private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {

		int left_sum = 0;// 記錄每次左邊找到的最大和值
		int sum = 0; // 記錄左邊子陣列所有元素的和值
		int max_left = -1;// 記錄每次找到左邊子陣列最大和值時的下標值

		for (int i = mid; i >= low; i--) {
			sum = sum + array[i];
			if (sum > left_sum) {
				left_sum = sum;
				max_left = i;
			}
		}

		int right_sum = 0;// 記錄每次右邊找到的最大和值
		sum = 0;//// 記錄右邊子陣列所有元素的和值
		int max_right = -1;// 記錄每次找到右邊子陣列最大和值時的下標值

		for (int j = mid + 1; j <= high; j++) {
			sum = sum + array[j];
			if (sum > right_sum) {
				right_sum = sum;
				max_right = j;
			}
		}

		MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);

		return obj;

	}

	public void arrayPrint(int[] array) {
		System.out.print("[");
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + ", ");
		}
		System.out.println("]");
	}
}
輸出如下:
MaxSumSubArray [left_index=7, right_index=10, sum=43]

求陣列的最大子陣列問題就解決了。