蓝桥杯 算法提高 和最大子序列(分治法和动态规划 c++)

和最大子序列(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

这道题其实有三种方法:

  1. 暴力法
  2. 分治法
  3. 动态规划

暴力

首先第一种暴力法,它的时间复杂度为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呢,我想这是我们在这道题中所要理解的东西,而不仅仅只是单纯的题解。

这篇文章我写了两天,两天的感受是不同的,第一天只是停留在:原来有这么多种方法,第二天写的时候我在反思为什么会从这一个方法转变成另一种方法,思考它其中的思路的一个转变,对于自己思维上是会有一个不小的突破。

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
最大连续序列和问题是指在一个序列中,找到一个连续的序列,使得它们的和最大。这个问题可以使用分治动态规划算法来解决。 1. 分治 使用分治求解最大连续序列和问题的思路是:将原序列分成两个部分,最大序列可能存在于左半部分、右半部分或跨越左右两个部分。分别计算这三种情况下的最大序列和,然后取最大值即可。 下面是使用分治求解最大连续序列和问题的Python代码示例: ```python def max_subarray(nums: List[int]) -> int: return _max_subarray(nums, 0, len(nums) - 1) def _max_subarray(nums: List[int], left: int, right: int) -> int: if left == right: return nums[left] mid = (left + right) // 2 left_max = _max_subarray(nums, left, mid) right_max = _max_subarray(nums, mid + 1, right) # 计算跨越左右两个部分的最大序列和 cross_max = nums[mid] left_cross_max = nums[mid] for i in range(mid - 1, left - 1, -1): left_cross_max += nums[i] cross_max = max(cross_max, left_cross_max) right_cross_max = nums[mid + 1] for i in range(mid + 2, right + 1): right_cross_max += nums[i] cross_max = max(cross_max, right_cross_max) return max(left_max, right_max, cross_max) ``` 在上面的代码中,我们使用递归的方式将原序列分成两个部分,然后计算跨越左右两个部分的最大序列和、左半部分的最大序列和、右半部分的最大序列和,取三者中的最大值作为整个序列的最大序列和。 2. 动态规划 使用动态规划算法求解最大连续序列和问题的思路是:从头开始遍历序列,对于每一个位置i,计算以i结尾的最大序列和,然后取所有的最大序列和的最大值作为问题的解。 具体来说,我们定义一个数组dp,其中dp[i]表示以i结尾的最大序列和。对于dp[i]来说,它的值可以由dp[i-1]和nums[i]计算得到,即dp[i] = max(dp[i-1] + nums[i], nums[i])。 下面是使用动态规划算法求解最大连续序列和问题的Python代码示例: ```python def max_subarray(nums: List[int]) -> int: dp = [0] * len(nums) dp[0] = nums[0] max_sum = nums[0] for i in range(1, len(nums)): dp[i] = max(dp[i-1] + nums[i], nums[i]) max_sum = max(max_sum, dp[i]) return max_sum ``` 在上面的代码中,我们使用一个循环遍历整个序列,计算以每一个位置为结尾的最大序列和,并且更新全局最大值。 希望这个回答能够帮到您!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值