/**
* -*- coding : GBK -*-
* @Author : huangshengjiang
* @Email : 1005542693@qq.com
* @Date : 2016-11-09 14:42
* @Last Modified Date : 2016-11-21 14:43
* @FileName : 最大子数组.cpp
*/
/*最大子数组
基本概念 : 在数组A中(有正有负),存在一段连续的子数组,其元素之和是最大的.例如有A[5] = {2,-7,4,-2,5},
其中最大子数组为A[2..4],特性是(一般情况下)数据有正有负.
应用场景 : 股票的应用(寻找最佳的买入和买出时间,已知后面的股票起伏).
具体需求 : 找出数组中的某个子数组,其元素之和是最大的.
输入 : 一组有正有负的数组
输出 : 最大子数组的左右坐标[1...n]范围内,最大子数组最小值为0,即(都是为负,不选择)
分析 : 以几何图为例,数据在图像上表现为上下起伏(如股票),要求出向上(not向下)的最高幅度(这样只要求出最小值最大值,
并且最小值的位置小于最大值的位置,概率为一半),如果最小值locmin在最大值locmax后面则需重新计算,概率为一半.
那么重新计算如何使用最大值,最小值的信息简化时间复杂度呢?
重新分析 : 这种有正有负的数据有两种表示方式后一个相对前一个的变化,大于为正,小于为负.
1.是以第一个数字为基础,对前一个的变化都计算出来,形成折线图.
所求的最大子数组在图像上的显示是某两点的差距(正向)是最大的.
2.是以0为基准,正在上,负在下的柱状图.
最大子数组在该图中的表现是两点之间的所有面积之和是最大的.
解决方案1/2 : 最直接最易懂的方法是一一尝试,求出所有可能的子数组,比较找出最大值的子数组输出.
优缺点 : 时间复杂度为O(n^3),太大不适合.可以改进,直接从a[i..j]的基础上计算a[i..j+1]的值,时间复杂度为O(n^2).
还可以改进.
方案1/2的思想都是遍历所有可能的排列数组,并相互比较,可不可以通过一些有效的判断,删去不需要的计算排列数组
解决方案3 : 分治思想,每次将当前数组等分为两部分,分别求出两部分的最大子数组,但这不一定是当前数组的最大子数组,
因为最大子数组有可能存在于跨越两部分.这里的关键性问题是如何确定中间部分的最大数组,其范围是在
左右最大子数组范围内.比较得出当前数组的最大子数组信息.
当前数组的中间最大子数组的可能范围是在[low,high]左右最大子数组范围内.但是有个特性是必然包含中间元素.
左右两端的数组可以不断的递归下去,直到递归基础left==right指向一个值,有logn次递归.
中间元素的求解从中间向两边各自扫描,找出其左右最大部分,相加就是中间最大子数组
优缺点 : 重复计算中间的扫描过程,但易于理解.
解决方案4 : 扫描思想,从结果找规律,
规律1 : 很明显无论怎样的数组(有正有负),其最大子数组的左右两端的值必定为正.两端外侧的值必然是负.
------------------------以下讨论有些价值---------------------------
规律2 : 反推法,已知[k,m]是数组中的最大子数组.则可以看成+-+-[k,m]-+-+.
其左右两端的负值绝对值总和一定大于正值.
规律2否决,本身就存在问题,一旦和为负值,舍弃(有可能是最大子数组),
因此舍弃的时候需要与当前最大值判断后,再舍弃
更根本的原因是非最大子数组也有这样的情况,而不是最大子数组独有的.因此规律2完全否决,以下的讨论否决
因此可根据这个结果规律,快速求出最大数组.
具体方法是从数组两端往中间扫描,两边都找到正值的位置i,j,可以明确的是最大子数组必然在[i,j]里.
关键是内部如何继续处理??
思考过程 : 内部如果都是正的,则当前i,j就是最大子数组.
如果内部有一个负的,则划分为左右两个正部分和中间一个负部分.最大值呢?极端点,如果负值过大,只能取正大的部分.
如果为-1的话,则最大就为这3部分之和.用数学方法来描述,则左部分值为sum_more,右部分为sum_less,中间负值为x.
有如下关系 : sum_more + sum_less - x 与 sum_more 的比较决定那部分为当前最大子数组.
也就是说x值要比其中较小的部分和大,则需要舍弃这较小正部分.
如果内部有2个负的,则将数组划分为3个正部分和2个负部分.
太麻烦!!
重新思考 : 基础是从数组两端往中间扫描,两边都找到首个正值的位置i,j.最大子数组在[i,j]里
如何继续处理 ?
根据规律2的说明是两端从左右正值向内扫描,
首先是左端开始同时累加,记录开始点,一旦出现和为负值,则舍弃该部分置和为0,继续扫描.
直到右端相遇.记录前一个开始扫描点i
然后是右端扫描同时累加,记录开始点,一旦和为负值,则舍弃该部分,开始点为下一个扫描点同时和置0,
直到和左端的扫描点相遇.记录前一个开始扫描点.j
因此此时[i,j]就是最大子数组.有问题,如果+-+,中间的负值远远大于另外正值.则根据上述方法得到的结果一直是右边的+.
不可,需要改正.
问题出在和出现负值的情况下舍弃之前的累加计算.(有可能舍弃了最大子数组)
---------------------------------------------------
继续重新思考 :
规律1 : 很明显无论怎样的数组(有正有负),其最大子数组(后选子数组)的左右两端的值必定为正.两端外侧的值必然是负.
从方案2的思考点出发,考虑较少不必要的计算.首先从左右两端往中间遍历,找到第一个正值i,j.然后从i开始正值遍历,直到遇到
负值.比较sum;等等,这种方法不是将[i,j]分割成多个正负相间的部分,并计算所有的可能性.如果正部分有n/2(最多),
负部分n/2-1个.则可能的组合有O(n^2)个,那能不能再减少组合次数呢?
结论 : 从左边正值开始遍历,累加正部分,并与当前的最大子数组比较,然后累加负部分,如果小于0,则从下一个正值开始累加;
如果大于0,则继续累加完正部分,在于当前最大子数组比较;直到遍历结束.
数学基础 : 找出+-+-+-+的顺序子数组和最大,最基础的+-+三部分,暂存首个+部分,计算+-,小于0,
则舍弃(可能是最大子数组保留下来),没有扩展的价值,从下一个+开始,大于0则表示有扩展的价值,此时+-+累加再比较.
则存储下+-+当前的最大子数组和保留有效的延展性.
优缺点 : 快速,时间复杂度为O(n).这是运用观察规律得到的结果
有4种解决方案,分别是立方,平方,分治,扫描两种.
*/
#include <iostream>
using namespace std;
template<class T>
int Size(T &array)
{
return sizeof(array)/sizeof(array[0]);
}
//函数
int Maxsum_simple(int *a,int n,int &left,int &right)//a表示指向数组的指针,n表示数组个数
//left,right是最大子数组的左右下标.范围在[0...n-1],返回值类型是int,表示最大子数组之和.
{
int sum = 0 ;
int tmp = 0 ;
for (int i = 0; i < n; ++i)
{
for (int j = i; j < n; ++j)
{
tmp = 0 ;
for (int k = i; k <= j ; ++k)
{
tmp += a[k] ;
}
if (tmp > sum )
{
left = i ;
right = j ;
sum = tmp ;
}
}
}
//这种方法是最简单的,时间复杂度为O(n^3),可以改进
return sum ;
}
int Maxsum_impsimple(int *a,int n,int &left,int &right)//a表示指向数组的指针,n表示数组个数
//left,right是最大子数组的左右下标.范围在[0...n-1],返回值类型是int,表示最大子数组之和.
{
int sum = 0 ;
int tmp = 0 ;
for (int i = 0; i < n; ++i)
{
tmp = 0 ;
for (int j = i; j < n; ++j)
{
tmp += a[j] ;
if (tmp > sum )
{
left = i ;
right = j ;
sum = tmp ;
}
}
}
//这种方法是最简单的,时间复杂度为O(n^2),还可以改进
return sum ;
}
/*int abs_test(int x)
{
return x > 0 ? x : -x ;
}*/
int Maxsum_scan(int *a,int n,int &left,int &right)//a表示指向数组的指针,n表示数组个数
//left,right是最大子数组的左右下标.范围在[0...n-1],返回值类型是int,表示最大子数组之和.
{
//关键是如何控制+-+之间的正负变化控制.功能是能够知道+-,-+的变化.abs(a)+abs(b) 与 abs(a+b) 比较,不同则产生变化
//相同则没有变化.除头尾两个abs一次,其余都需要abs两次.总共需要abs 3n次.可以用某个值替代abs(b),减少计算.
//返回值
int sum = 0 ;
// a和b暂存abs的数据
int a_tmp = abs(a[0]);
int b = 0 ;
//暂存某个子数组相关数据, 注意点是遍历的开始和结束处理.
int tmp_sum = a[0] > 0 ? a[0] : 0 ;
int tmp_left = -1 ; //指向左边正值的前一个负值下标
for (int i = 1; i < n; ++i)
{
b = abs(a[i]) ;
if (a_tmp+b != abs(a[i-1]+a[i]) && a[i] < 0)//发生符号变化,+到-,需要比较保留,或-到+需要继续
{
//与当前子数组比较
if (tmp_sum > sum)
{
sum = tmp_sum ;
left = tmp_left + 1 ;
right = i-1 ;
}
}
tmp_sum += a[i] ;
if (tmp_sum < 0)//和小于0,舍弃
{
tmp_sum = 0 ;
tmp_left = i ;
}
a_tmp = b ;
}
//遍历截止,若为正则需比较. 注意点是遍历的开始和结束处理.
if (tmp_sum > sum)
{
sum = tmp_sum ;
left = tmp_left + 1 ;
right = n-1 ;
}
return sum ;
}
//设置数据结构,存放当前数组的相关信息
struct array_messege
{
int sum ; //最大子数组和
int left ; //最大子数组左下标
int right ; //最大子数组右下标
array_messege(int left_,int right_) : left(left_),right(right_),sum(0){}
};
int Maxsum_div_detail(int *a,int n,int left_org,int right_org,int &left,int &right)//a表示指向数组的指针,n表示数组个数
//left_org,right_org表示当前数组的划分,left,right是求得的最大子数组的左右下标.范围在[left_org...right_org],
//返回值类型是int,表示当前最大子数组之和.
{
if (left_org == right_org )//递归基础 : left == right ,此时当前数组只有一个元素,其值为正,取该值,否则为负取0.
{
return a[left_org] > 0 ? a[left_org] : 0 ;
}
int mid = (left_org + right_org) / 2 ;
array_messege l(left_org , mid ) ; //表示左边数组的最大子数组的信息
array_messege r(mid+1 ,right_org ) ;//表示右边数组的最大子数组的信息
l.sum = Maxsum_div_detail(a,n,left_org,mid,l.left,l.right);//求出左边最大子数组信息
r.sum = Maxsum_div_detail(a,n,mid+1,right_org,r.left,r.right);//求出右边最大子数组信息
//重点求出中间的最大子数组信息.其特点是一定跨越其中间边界,其实用到扫描的思想
int lmax , rmax , temp ,left_m = mid, right_m = mid + 1 ; //lmax,rmax左/右最大值,temp暂存结果用于比较.
//最后存储lmax+rmax;left_m,right_m为中间的左右边界
lmax = temp = 0 ;
for (int i = mid ; i >= left_org ; i--)
{
temp += a[i] ;
if (temp > lmax)//不能有等号,如果出现极端现象,都是负值
{
lmax = temp ;
left_m = i ;
}
}
rmax = temp = 0 ;
for (int i = mid + 1; i <= right_org ; ++i)
{
temp += a[i] ;
if (temp > rmax)
{
rmax = temp ;
right_m = i ;
}
}
array_messege m(left_m,right_m) ;
m.sum = lmax + rmax ;
//比较三者的大小
array_messege tmp(left_org,right_org) ;
tmp = l.sum > r.sum ? l : r ; // 此时temp作为存放最大值的变量(容器)
tmp = tmp.sum > m.sum ? tmp : m ; //三者的比较不能有等号,否则会引起歧义(如 left有两个值),最好设置数据结构.消除二义性
left = tmp.left ;
right = tmp.right ;
return tmp.sum ;
}
int Maxsum_div(int *a,int n,int &left,int &right)
{
return Maxsum_div_detail(a,n,0,n-1,left,right) ;
}
//测试
int main(int argc, char const *argv[])
{
int a[] = {31,-41,58,97,-53,59,26,-93,-23,84};//{31,-41,59,26,-53,58,97,-93,-23,84};
int n = Size(a);
int left,right;
int result = Maxsum_div(a,n,left,right);
cout << "左下标为" << left << "右下标为" << right << endl;
cout << "和为" << result <<endl;
system("pause");
return 0;
}