和最大子序列(c++)
题目
资源限制
时间限制:1.0s 内存限制:512.0MB
问题描述
对于一个给定的长度为N的整数序列A,它的“子序列”的定义是:A中非空的一段连续的元素(整数)。你要完成的任务是,在所有可能的子序列中,找到一个子序列,该子序列中所有元素的和是最大的(跟其他所有子序列相比)。程序要求你输出这个最大值。
输入格式
输入文件的第一行包含一个整数N,第二行包含N个整数,表示A。
其中 : 1 <= N <= 100000,-10000 <= A[i] <=10000
输出格式
输出仅包含一个整数,表示你算出的答案。
样例输入
5
3 -2 3 -5 4
样例输出
4
这道题其实有三种方法:
- 暴力法
- 分治法
- 动态规划
暴力
首先第一种暴力法,它的时间复杂度为O(n3),效率低,代码就不贴出来了,这个方法大概率是会超时的。
分治
然后是第二种,分治法,这个方法的思想很不错,(快速排序也是这个思想),简单描述一下就是一分为二和递归,下面详细讲解一下:
分治是分四步走:
- 找到左边数组中的和最大子序列
- 找到右边数组中的和最大子序列
- 从中间往两边找和最大子序列(min1 + min2的值是整个小区间的和)
- 递归
tip:
我自己作为一个小白,在研究这个地方的时候其实对于max3是有点疑问的,因为我一开始不太明白为什么从中间往两边找最大要min1+min2,我总感觉中间区间可能是不连续的,但是经过了老师的提醒过后我发现,其实这是递归啊,最一开始就是要先找到最小的那个数组,(抽象想一想),一个一个拆成一半,然后最后再往里和,是一个展开又聚拢的过程,这同样也能够解释了为什么能够将一开始的for循环换成现在这样。
(反过来想一下,如果左边找出来的最大值比min1大,那说明左边的最大和的子序列就肯定不是连续区间,整个区间也就肯定不是连续的)(连续是指整个左边或者右边区间)
找Max1、Max2、Max3中的最大值,即最终答案。
代码如下:
#include<iostream>
using namespace std;
int n,a[200005];
const int min0 = -10000000;
inline int max(int a,int b)//求max
{
return a > b ? a : b;
}
int fz(int l,int r)//分治法
{
if(l == r)
{
return a[l];//相当于是如果就一个了就返回这个值
}
int mid = (l + r) / 2;
int sum = 0,min1 = min0,min2 = min0;
for(int i = mid;i >= 0;i--)//求区间最大值(左边一半区间)
{
sum += a[i];
min1 = max(min1,sum);
}
sum = 0;
for(int i = mid + 1;i <= r;i++)//求区间最大值(右边一半区间)
{
sum += a[i];
min2 = max(min2,sum);
}
return max(max(fz(l,mid),fz(mid + 1,r)),min2 + min1);
//返回分治法中三种可能性的最大值;
}
int main()
{
cin>>n;
for(int i = 0;i < n;i++)
{
cin>>a[i];
}
cout<<fz(1,n)<<'\n';
return 0;
}
但是这个方法因为经过多次的累加所以比较慢,会超时,所以如果能够把累加的步骤省取那么程序的速度会稍微快了一点
下面这个代码是改进过后的:
#include<iostream>
using namespace std;
int n,a[200005];
const int min0 = -10000000;
int sum[200006]={0}; //sum[i]表前前i个元素的和,如sum[5]为0-4元素的和
int fz(int l,int r)//分治法
{
if(l == r)
{
return a[l];
}
int mid = (l + r) / 2;
int min1 = min0,min2 = min0;
//左区间[l,mid]
if(min1<sum[mid]-sum[l])
min1=sum[mid]-sum[l];
//右区间[mid+1,r-1]
if(min2 < sum[r]-sum[mid])
min2 = sum[r]-sum[mid];
return max(max(fz(l,mid),fz(mid + 1,r)),min2 + min1);
//返回分治法中三种可能性的最大值;
}
int main()
{
cin>>n;
for(int i = 0;i < n;++i)
{
cin>>a[i];
sum[i+1]=sum[i]+a[i];
}
// for(int i=1;i<n+1;++i)
// {
// cout<<sum[i]<<" ";
// }
cout<<fz(0,n)<<'\n';
return 0;
}
但其实这个代码还可以改进,这个分治中的两个if其实都没有意义,仔细来看,这个分治中的min1和min2在每一次递归的时候都会被重新赋值,所以根本没有必要去进行判断,直接后期在递归中已经判断了,没有必要再进行重复判断,同时提高了速度。
代码如下:
#include<iostream>
using namespace std;
int n,a[200005];
//const int min0 = -10000000;
int sum[200006]={0};//sum[i]表前前i个元素的和,如sum[5]为0-4元素的和
int fz(int l,int r)//分治法[l,r-1]
{
if(l == r-1)
{
return a[l];
}
int mid = (l + r-1) / 2;
int min1,min2;
//左区间[l,mid]
min1=sum[mid+1]-sum[l];
//右区间[mid+1,r-1]
min2 = sum[r]-sum[mid+1];
return max(max(fz(l,mid+1),fz(mid + 1,r)),min2 + min1);
//返回分治法中三种可能性的最大值;
}
int main()
{
cin>>n;
for(int i = 0;i < n;++i)
{
cin>>a[i];
sum[i+1]=sum[i]+a[i];
}
cout<<fz(0,n)<<'\n';
return 0;
}
动态规划
最后一个方法就是非常快,时间复杂度在O(n),从头到尾的方法,从n3到n会感觉是一种质的飞跃,很奇妙的感觉。
这题也算是动态规划的入门题,动态规划首先是有两点:局部最优和无后效性,这道题主要利用了一个思想叫“前n个元素中以第n个元素为结尾的最大子序和”,因为本题要求的是前n个元素的最大子序和,但是无法从中得到一个递推的关系,所以要用到这个思想,从而写出状态转移方程。
一开始看的时候可能会有疑惑,这个dp[n]数组到底是什么??它其实是代表以n结尾的最大子序和,判断其是否为正,是否能够为下一个数起到正增益的一个效果,如果能够起到增加的效果那么将它再加上下一个数,否则直接到下一个数,然后和前一个dp进行比较,取大的那个。
说的直白一点就是,把以每一个数为结尾的最大子序和放到一个数组里,取出最大的那个,就是我们要求的那个。
#include<iostream>
using namespace std;
int dp[200005],num[200005],n,ans = -10000000;
int main()
{
cin>>n;
cin>>num[0];
dp[0] = num[0];
ans = dp[0];
for(int i = 1;i < n;i++)
{
cin>>num[i];
dp[i] = max(dp[i - 1],0) + num[i];//状态转移方程
ans = max(ans,dp[i]);
}
cout<<ans<<'\n';
return 0;
}
又来但是了,这个代码也可以优化,其实如果不用dp数组存储其实完全可以是实现,相当于节省了空间,代码如下:
#include<iostream>
using namespace std;
int n,ans = -10000000,num[200005],b;
int main()
{
cin>>n;
cin>>num[0];
b = num[0];
ans = b;
for(int i = 1;i < n;i++)
{
cin>>num[i];
b = max(b,0) + num[i];//状态转移方程
ans = max(ans,b);
}
cout<<ans<<'\n';
return 0;
}
总结
三种方法各有优劣,虽然后面的方法速度和空间上都有一定的减少,但是同时也是很难想到的,动态规划只是一种思想,如果想要用到实际的题目中,光是一个状态转移方程就挺难想到,但是稍微比它慢一点的分治法其实算是比较好想到的。
同时,在这道题目中我们应该要明白,我们学会了什么,三种方法是一种思维上的跃迁,虽然我们很难想到将nlogn降低到n,但是当我们碰到n2时,能否再简化将其变成nlogn呢,我想这是我们在这道题中所要理解的东西,而不仅仅只是单纯的题解。
这篇文章我写了两天,两天的感受是不同的,第一天只是停留在:原来有这么多种方法,第二天写的时候我在反思为什么会从这一个方法转变成另一种方法,思考它其中的思路的一个转变,对于自己思维上是会有一个不小的突破。