最大的子序列和问题

    最大的子序列和问题是一个很经典的问题,各种考试面试中也经常碰到。这问题的解决不难,关键是通过这个问题体会一些算法的思路,学习思考怎么解决问题。

    问题是这样的:给定整数A1,A2,...,An(正负不限),Ai,...,Aj(1<=i<=j<=n)是其一个子序列,Sum为此子序列所有元素的和,对所有可能的i,j,求Sum的最大值。例如:对于输入-2,-1,11,3,5,-8,-2,9,-20,-3,5,18,-5,3,答案为23(从A11到A12)。

    最直接也最容易想到的办法(当然也就是最暴力的方法),就是求出所有子序列的和,然后找出最大的。按照这个思路写出的C++代码如下:

int Method1(const vector<int> & v)
{
	int max = v[0];
	int size = v.size();
	for(int indexSubSerBeg=0; indexSubSerBeg<size; indexSubSerBeg++)
	{
		for(int indexSubSerEnd=indexSubSerBeg; indexSubSerEnd<size; indexSubSerEnd++)
		{
			int subSerSum = v[indexSubSerBeg];
			for(int i=indexSubSerBeg+1; i<indexSubSerEnd; i++)
			{
				subSerSum += v[i];
			}
			if(subSerSum > max)
			{
				max = subSerSum;
			}
		}
	}
	return max;
}
    目测有三层循环,显然其时间复杂度是O(N^3)。数据量稍大,这速度几乎就不可容忍了。于是要想想怎么减少循环的层次。其实这就是一个思考的过程,往哪个方向去想这就是体现能力的地方。

    观察一下这个序列很容易发现,-2,-1,11,3这个子序列的和就是-2,-1,11这个子序列的和加上3。这样求一个子序列的和可以利用前面计算的子序列和,能减少一些计算量。按这个思路写出的代码如下:

int Method2(const vector<int> & v)
{
	int max = v[0];
	int size = v.size();
	for(int indexSubSerBeg=0; indexSubSerBeg<size; indexSubSerBeg++)
	{
		int subSerSum = 0;
		for(int indexSubSerEnd=indexSubSerBeg; indexSubSerEnd<size; indexSubSerEnd++)
		{
			subSerSum += v[indexSubSerEnd];
			if(subSerSum > max)
			{
				max = subSerSum;
			}
		}
	}
	return max;
}
    少了一层循环,时间复杂度减少到了O(N^2)。比之前的结果要好多了。如果是要求出所有子序列的和,因为子序列有N(N+1)/2个,所以时间复杂度就只能降到这个程度了。

    然而再读一下题,发现题目并没有要求我们求出所有的子序列和,只是要求求出最大的子序列和,所以应该还有优化的余地。我们再观察,看能不能再减少计算量。花点时间,应该可以发现这么两个特征:

    1. 如果序列全是非正数,那么最终结果就是最大的非正数。

    2. 如果序列有正数,那么最大子序列的开始位置一定是一个正数,结束位置也一定是一个正数。(这一条反证很容易)

    所以我们可以找出所有从正数开始的子序列的最大和,可以尝试用一次遍历就实现。经过一番简化和验证,得到如下代码:

int Method3(const vector<int> & v)
{
	int size = v.size();
	assert(size>0);
	int partSum = v[0];
	int max = partSum;
	for(int i=1;i<size;i++)
	{
		if(partSum<0)
			partSum = 0;
		partSum += v[i];
		if(partSum > max)
			max = partSum;
	}
	return max;
}
    一次循环就搞定,时间复杂度为O(N)。这就是这个问题最小的时间复杂度了,因为至少要遍历子序列一遍,所以不用再想减少时间复杂度了,这算法看起来也足够简单。

    但是我们还可以思考,还有没有其他的方法呢?当然是有的,不考虑效率的话,方法无数多。有一个经常提到的方法是用分治,这也是常用的思路,不过对这题来说不是那么容易想到。事实上,我就没想到。这个方法的核心思想是:把序列分成左右两部分,最大子序列和要么出现在左部分,要么出现在右部分,要么跨越两部分中间连着。于是可以递归实现。代码如下:

int Method4(const vector<int> & v, int left, int right)
{
	if(left>=right)
		return v[left];

	int mid = (left+right)/2;
	int leftSum = v[mid];
	int leftMax = leftSum;
	for(int i=mid-1; i>0; i--)
	{
		leftSum += v[i];
		if(leftSum>leftMax)
			leftMax = leftSum;
	}

	int rightSum = v[mid+1];
	int rightMax = rightSum;
	for(int i=mid+2; i<v.size(); i++)
	{
		rightSum += v[i];
		if(rightSum>rightMax)
			rightMax = rightSum;
	}

	return Max3(leftMax+rightMax, Method3(v,left,mid), Method3(v,mid+1,right));
}

int Max3(int n1, int n2, int n3)
{
	int max = n1;
	if(n2>max)
		max = n2;
	if(n3>max)
		max = n3;
	return max;
}
    这个算法的时间复杂度是O(NlogN)。


发布了63 篇原创文章 · 获赞 16 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/tyst08/article/details/9622059