求數組最大子序和問題--動態規劃和分治思想實現

求給定數組的最大子序和

題目描述:
給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。

示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6

解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
進階:
如果你已經實現複雜度爲 O(n) 的解法,嘗試使用更爲精妙的分治法求解。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/maximum-subarray

暴力實現法:

分析:要找出數組中的最大自序和,最直接的就是將每個元素向後求長度爲1,爲2,……等的求和,取最大序和,枚舉出每段的累加和,這樣每個元素都對應一個向後遍歷的最大自序和,一共有numsSize!個值,然後在所有元素對應的最大自序和中取最大的作爲該數組的最大自序和,這樣簡單暴力的思想也很容易想到。
代碼實現:

 int add_arr_i_j(int *arr, int start, int end)//計算並返回start到end下標的累加和
{
	int num = 0;
	int i = 0;
	for (i = start; i <= end; i++)
	{
		num += arr[i];
	}
	return num;
}
int maxSubArray(int* nums, int numsSize)
{
	int i = 0;
	int num = 0;//num是從i-j的子序列和
	int num_max = nums[0];//記錄每個i值的最大序列和
	int arr_max = nums[0];//整個數組中的子序列最大和
	for (; i < numsSize; i++)
	{
		int j = 0;
		//num_max = nums[0];
		//計算從i到n中的最大子序列,並將下標pos入vector
		for (j = i; j < numsSize; j++)
		{
			//計算i-j的和
			num = add_arr_i_j(nums, i, j);//num爲求出的和
			if (num_max < num)//在一行隊列中挑選最大的
			{
				num_max = num;
			}
		}
		if (arr_max < num_max)//取每個i個最大子序列,在每行隊列中最大的數組成的數組中選出最大的
		{
			arr_max = num_max;
		}
	}
	return arr_max;
}

通過從前向後遍歷每個元素,選取該元素開始向後的最大自序和,然後再這些元素的最大自序和中選取最大的作爲該數組的最大子序和。算法簡單暴力,只是在LeetCode上提交的時候不能通過,原因是時間複雜度太高爲O(n^3),還是時間複雜度的問題,那麼如何提高時間複雜度呢?可以用新的數組空間保存從數組首地址開始的累加和,每一段的累加和就爲array[end]-arrayp[start],可使時間複雜度降爲O(n^2)。那麼是否可以在不增加空間複雜度的情況下降低時間複雜度呢?

動態規劃方法:

從前向後遍歷,用tmp_sum變量記錄向後遍歷的累加和(就是每段開始到該下標的累加和),若在this_sum的累加過程中遇到前一個this_sum爲負數時,該this_sum就不能給後續的累加帶來增益,因此放棄前面的this_sum(可置0或賦下一個數組值),有一種去其糟粕的意思 ! 同時也標示着this_sum開始記錄一個新段的累計和。
用max_sum記錄第一段的最大自序和,在某段this_sum的累加過程中當this_sum大於max_sum時,更新max_sum的值 ;最後得到的max_sum值爲數組中最大自序和的值。
整體思想就是tmp_sum不斷更新記錄每一個段的自序和,並不斷更新max_sum的值,使max_sum取這些自序和中最大的。最後得到最大自序和!
LeetCode題解中的一個幫助理解的例子:
在這裏插入圖片描述
狀態轉移方程爲: tmp_sum = max(tmp_sum+ nums[i], nums[i])
代碼實現:

int FindGreatestSumOfSubArray(vector<int> array)
{
	int i = 0;
	int max_sum = array[0];
	int this_sum = array[0];
	for (i = 1; i < array.size(); i++)//循環遍歷數組,
	{
		if (this_sum + array[i]>array[i])//如果當前疊加的值比上一個疊加的值小,說明this_sum爲負值,則重新開始
		{
			this_sum += array[i];//此時將最大的值賦給max_num
		}
		else
		{
			this_sum = array[i];
		}
		//this_sum每次記錄當前疊加之後的值和array[i]之間的較大值,當前面的記錄的值爲負值時,後面重新開始this_sum=array[i];(既往不咎,因爲前面的爲負,計算上前面的值之後只會越來越小)
		if (max_sum < this_sum)//每累加一次,對max_sum進行一次判斷更新
		{
			max_sum = this_sum;
		}
	}
	return max_sum;//返回最大的疊加值
}

以示例做例子圖解:
在這裏插入圖片描述
從圖中可以看出示例中max_sum取自第三段的最大自序和。
分析上面的算法時間複雜度是O(n),已經可以通過LeetCode的提交了,那麼是否還有更優越的算法?從題中我們無從得知這個最大子序列是從哪個下標開始,哪個下標結束,而且上面的動歸算法已經提示我們最大自序和來自數組中的某一段,那我們可以先將該數組分成許多小段,然後分別計算這些分段的最大自序和,再計算合段後的最大自序和,取最大的一段作爲最大子序和。這就是分治法的思想。

分治法

將數組拆分爲小段,每次合段的時候比較兩端的最大自序和及合段後的最大自序和,合段後的最大自序和就是從mid向兩端遍歷不斷累加的最大值,返回三數中的最大值作爲該段的最大子序和。然後不斷向上層遞歸可得到整個數組的最大自序和。

int dealmaxSubArray(vector<int> &nums,int start,int end)
{
	if (start >= end)
	{
		cout << start << " nums[start]:";
		cout << nums[start] << endl;
		return nums[start];
	}
	int i = 0;
	int mid = start + (end - start) / 2;
	int le_val=dealmaxSubArray(nums,start,mid);//返回左段最大子序和
	int rg_val=dealmaxSubArray(nums, mid+1, end);//返回右段最大自序和
	int sum = 0;
	int tmpmax = nums[mid];
	int crossmax = 0;//記錄合段後的段最大自序和
	for (i = mid; i >= start; i--)
	{
		sum += nums[i];
		if (sum > tmpmax)
		{
			tmpmax = sum;
		}
	}
	crossmax = tmpmax;
	sum = 0;
	tmpmax = nums[mid+1];
	for (i = mid + 1; i <= end; i++)
	{
		sum += nums[i];
		if (sum > tmpmax)
		{
			tmpmax = sum;
		}
	}
	crossmax += tmpmax;
	return max(max(le_val, rg_val), crossmax);
}

int maxSubArray(vector<int>& nums)
{
	int n = nums.size();
	int val=dealmaxSubArray(nums, 0, n-1);//下標從0~n-1
	return val;
}

分治法的時間複雜度爲O(nlogN),相比於遞歸其實也差不多,不過比較不容易想到。
這裏安利一個Github上的題解:https://github.com/azl397985856/leetcode/blob/master/problems/53.maximum-sum-subarray-cn.md#解法二—前綴和–暴力解裏面的講的解法三 - 優化前綴算法還是挺漂亮的。

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