【最大子数组问题】之分治策略分析

1.【问题描述】

给定一个有效指定数组,找出其一连续子数组(该子数组的连续元素在原数组中也是连续的,例如:给定数组[1,2,3,4,5],其中[2,3,4]是其连续子数组,但是[1,3,4]不是),使得该连续子数组的值和为所有连续子数组中最大。

2. 暴力求解方法

可以很容易地设计出一个暴力求解算法就行分析,即:简单的去尝试所有的连续子数组,找出其最大的一个即可。假设其处理问题的规模为 n n n,那么我们需要尝试所有的可能性,根据组合知识可知,所有的可能性有 ( 2 n ) = n ! 2 ∗ ( n − 2 ) ! = Θ ( n 2 ) \begin{pmatrix}2 \\n \end{pmatrix}=\frac{n!}{2 * (n-2)!}=\Theta(n^2) (2n)=2(n2)!n!=Θ(n2)种,而处理每种情况所花费的时间至少也是常量,则整个算法的运行时间的算法复杂度至少为 Ω ( n 2 ) \Omega(n^2) Ω(n2)【渐近下界】, Ω ( n 2 ) \Omega(n^2) Ω(n2)的时间复杂度在算法设计方面是一个比较大的值,尽可能去尝试具有更小复杂度的算法。

3. 分治策略用于最大子数组问题的算法分析

【分治策略用于解决递归问题】:

分治策略用于解决一个递归问题时,在每层的递归中应用如下三个步骤:
(1)分解:将原问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
(2)解决:递归求解出子问题,如果子问题的规模足够小,则停止递归。
(3)合并:将子问题的解组合成原问题的解。

3.1 分解步骤

(1)原问题描述为:对于一个给定规模为 n n n的的子数组,求解出其具有最大和值的连续子数组。
(2)规模更小的子问题为:对于一个给定规模为 m ( m &lt; n ) m(m&lt;n) mm<n)的子数组,求解出其具有最大和值的连续子数组。

分析:
(1)已知 m &lt; n m&lt;n m<n,则分解出的所有的子问题的规模和则与原问题相等,为了便于分析,将子问题分解为两个相同规模的问题,即: m = n / 2 m=n/2 m=n/2(在m为奇数情况下,两个问题的规模相差1)。现在我们需要分析的是划分出的子规模的问题的解的合并是否能够充分解决原问题。
(2)假设两个子问题 A 1 , A 2 A_1,A_2 A1,A2是原问题划分出来的两个同等规模的问题, A 1 A_1 A1的问题的解必定存在于 A 1 A_1 A1原数组中, A 2 A_2 A2亦然。
(3)但是对于原问题,从划分出来的子问题的角度看,我们容易得出其最终解可能存在于 A 1 A_1 A1中,也可能存在于 A 2 A_2 A2中,当然也可能一部分存在于 A 1 A_1 A1,一部分存在于 A 2 A_2 A2中(即跨两个子问题存在)。

(3)如上分析可知,在将原问题划分为两个相同规模的子问题后,其解的合并也不能准确求解出终解,因为缺少一种分析。则我们在两个子问题的基础上再增加一个子分支3(不属于原问题形式,或者说是原问题形式的一个限制形式):
给定两个有效数组,求解跨两个数组的最大子数组(所谓跨两个数组的意思是最后得到的最大子数组元素在两个子数组中都有存在,即至少是包含左边数组的最后一个元素和右边数组的第一个元素。)

至此,将原问题的递归步骤分解为两个同等规模的相同形式的原问题的子问题1,2和一个加了限制条件的子问题分支3,则分解步骤完成。

3.2 解决步骤

(1)子问题1,2的解决方案与原问题是同一问题,在最后合并步骤可简单的通过递归求得。
(2)子问题分支3的解决方案:(以整数的数组为示例,以下c++示例程序仅用来说明算法的求解步骤,不做程序安全性、稳定性等方面考量)。

首先,我们先定义一个数据结构SubArrayInfo来存储求解数组的相关信息:

#include <limits.h>
#include <iostream>
#include <math.h>
using namespace std;

struct SubArrayInfo
{
	SubArrayInfo(int left=-1, int right=-1, int sum=INT_MIN)
	{
		max_left = left;
		max_right = right; 
		SubArraySum = sum; 
	}
	int max_left;   //求解最大子数组的最小索引
	int max_right; //求解最大子数组的最大索引
	int SubArraySum;  //求解最大子数组的值和
};

然后,我们定义求解最大跨越子数组的问题函数:

/**
* @param[in] Array-输入原数组,
* @param[in] mid-分割数组的索引位置(将数组分割为左右两个子数组,索引位置包含在左数组中
* @param[in] low-数组的最小索引,high-数组的最大索引
* @return 求解出的子数组信息,由SubArrayInfo结构给出
*/
SubArrayInfo FindMaxCrossingSubArray(int* Array, int mid, int low, int high)
{
	SubArrayInfo ret;
    //求解经mid分割后的左数组的最大连续子数组(由于必须包含mid索引值,所以只需要向前顺序遍历即可。
	int left_sum = INT_MIN; //在求解前将最终和值先置为负无穷
	int left_sum_tmp = 0;

	for(int i = mid;i>low-1; i--)
	{
		left_sum_tmp += Array[i];
		if(left_sum_tmp > left_sum)
		{
			left_sum = left_sum_tmp;
			ret.max_left = i;
		}
	}
	//同上理求解右数组的最大连续子数组
	int right_sum = INT_MIN;
	int right_sum_tmp = 0;

	for(int i = mid+1;i<high+1; i++)
	{
		right_sum_tmp += Array[i];
		if(right_sum_tmp > right_sum)
		{
			right_sum = right_sum_tmp;
			ret.max_right = i;
		}
	}

	ret.SubArraySum = left_sum + right_sum;

	return ret;
}

3.3 合并步骤

将上述递归分析进行问题合并,即通过递归求解整个问题:

#include <limits.h>
#include <iostream>
#include <math.h>
using namespace std;

SubArrayInfo FindMaxSubArray(int* Array, int low, int high)
{
	if(low == high)  //min_divide solution or output of recurrence
	{
		return SubArrayInfo(low, high, Array[low]);
	}
	else //solve the max_value among left,right and corss,  return max_value
	{
		int mid = floor((low+high)/2);
		SubArrayInfo left_max = FindMaxSubArray(Array,low,mid); //求解左数组的最大连续子数组
		SubArrayInfo right_max = FindMaxSubArray(Array,mid+1, high); //求解右数组的最大连续子数组
		SubArrayInfo cross_max = FindMaxCrossingSubArray(Array, mid,low,high); //求解跨左右数组的最大连续子数组

		//经过比较求解出最大子数组
		SubArrayInfo output_info;
		left_max.SubArraySum > right_max.SubArraySum ? (output_info = left_max) : (output_info = right_max);
		if(output_info.SubArraySum < cross_max.SubArraySum)
		{
			output_info = cross_max;
		};
		return output_info;
	}
}

3.4 完整示例程序

#include <limits.h>
#include <iostream>
#include <math.h>
using namespace std;

struct SubArrayInfo
{
	SubArrayInfo(int left=-1, int right=-1, int sum=INT_MIN)
	{
		max_left = left;
		max_right = right;
		SubArraySum = sum;
	}
	int max_left;
	int max_right;
	int SubArraySum;
};

/*
	@note: left_section: low <= left <=mid; mid < right_section <= right;
*/
SubArrayInfo FindMaxCrossingSubArray(int* Array, int mid, int low, int high)时间复杂度为Θ(n)
{
	SubArrayInfo ret;


	int left_sum = INT_MIN;
	int left_sum_tmp = 0;

	for(int i = mid;i>low-1; i--) //时间复杂度为Θ(n/2)
	{
		left_sum_tmp += Array[i];
		if(left_sum_tmp > left_sum)
		{
			left_sum = left_sum_tmp;
			ret.max_left = i;
		}
	}

	int right_sum = INT_MIN;
	int right_sum_tmp = 0;

	for(int i = mid+1;i<high+1; i++)  //时间复杂度为Θ(n/2)
	{
		right_sum_tmp += Array[i];
		if(right_sum_tmp > right_sum)
		{
			right_sum = right_sum_tmp;
			ret.max_right = i;
		}
	}

	ret.SubArraySum = left_sum + right_sum;

	return ret;
}


SubArrayInfo FindMaxSubArray(int* Array, int low, int high) //假设时间复杂度为T(n)
{
	if(low == high)  //min_divide solution or output of recurrence -时间复杂度Θ(1)
	{
		return SubArrayInfo(low, high, Array[low]); //-时间复杂度Θ(1)
	}
	else //solve the max_value among left,right and corss,  return max_value
	{
		int mid = floor((low+high)/2); //-时间复杂度Θ(1)
		SubArrayInfo left_max = FindMaxSubArray(Array,low,mid);//时间复杂度为T(n/2)
		SubArrayInfo right_max = FindMaxSubArray(Array,mid+1, high); //时间复杂度为T(n/2)
		SubArrayInfo cross_max = FindMaxCrossingSubArray(Array, mid,low,high); //时间复杂度为Θ(n)

		//以下时间复杂度为Θ(1)
		SubArrayInfo output_info;
		left_max.SubArraySum > right_max.SubArraySum ? (output_info = left_max) : (output_info = right_max);
		if(output_info.SubArraySum < cross_max.SubArraySum)
		{
			output_info = cross_max;
		};
		return output_info;
	}
}
		
int main()
{
	int a[6] = {1,-3,-2,-6,3,-1};

	SubArrayInfo info = FindMaxCrossingSubArray(a, 2, 0, 5);
	cout << "info.max_left= " << info.max_left<< endl;
	cout << "info.max_right= " << info.max_right << endl;
	cout << "info.sum= " << info.SubArraySum << endl;

	cout << endl <<"FindMaxSubArray>>" << endl;
	info = FindMaxSubArray(a, 0, 5);
	cout << "info.max_left= " << info.max_left<< endl;
	cout << "info.max_right= " << info.max_right << endl;
	cout << "info.sum= " << info.SubArraySum << endl;

	return 0;
}

3.5 时间复杂度分析

由于我们将原问题分解成了两个相同规模的子问题1,2和一个子问题分之问题3,我们假设原问题的规模 n n n问2的幂次方(保证子问题规模 n / 2 n/2 n/2为整数)。

(1)分析FindMaxCrossingSubArray函数的时间复杂度:
FindMaxCrossingSubArray函数的主要耗时部分在两个for循环中。
假设FindMaxSubArray函数的运行时间复杂度为 T ( n ) T(n) T(n),针对各个部分的时间复杂度在程序中均进行了标注。
(i)在 n = 1 n=1 n时, T ( n ) = Θ ( 1 ) T(n)= \Theta(1) T(n)=Θ(1)
(ii)在 n &gt; 1 n&gt;1 n>1时, T ( n ) = Θ ( 1 ) + 2 T ( n / 2 ) + Θ ( n ) + Θ ( 1 ) = 2 T ( n / 2 ) + Θ ( n ) T(n)=\Theta(1)+2T(n/2)+\Theta(n)+\Theta(1)=2T(n/2)+\Theta(n) T(n)=Θ(1)+2T(n/2)+Θ(n)+Θ(1)=2T(n/2)+Θ(n),求解该递归式子,可得 T ( n ) = Θ ( n l g n ) T(n)=\Theta(nlgn) T(n)=Θ(nlgn)
则基于递归的方法能将其时间复杂度降低到 T ( n ) = Θ ( n l g n ) T(n)=\Theta(nlgn) T(n)=Θ(nlgn)

还有其他的算法可进一步降低其时间复杂度,本例仅用来阐述基于递归分治法的在本方面的应用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值