最大连续子数列求和的优化历程

最大子数列求和

今天在别人的博客里最大连续子数列和学到了很多,在此总结一下自己的学习历程

1.暴力递归 O(N²)

用数组sum[i]表示第1个到第i个数的和,用sum[j] - sum[i-1]表示第i个到第j个这个数列的和,省掉最内层的循环,C语言代码如下:

#include <stdio.h>

//N是数组长度,num是待计算的数组,sum是数组前缀和,放在全局区是因为可以开很大的数组
int N, num[16384], sum[16384];

int main()
{
    //输入数据
    scanf("%d", &N);
    for(int i = 1; i <= N; i++)
        scanf("%d", &num[i]);
    
    //计算数组前缀和
    sum[0] = 0;
    for(int i = 1; i <= N; i++) {
        sum[i] = num[i] + sum[i - 1];
    }

    int ans = num[1]; //ans保存最大子数列和,初始化为num[1]能保证最终结果正确
    //i和j分别是枚举的子数列的起点和终点
    for(int i = 1; i <= N; i++) {
        for(int j = i; j <= N; j++) {
            int s = sum[j] - sum[i - 1];
            if(s > ans) ans = s;
        }
    }
    printf("%d\n", ans);

    return 0;
}

依照博主的意思,还可以再优化一下,用num数组直接去存和,代替sum素组:

#include <stdio.h>
int N, num[16384];

int main() {
	//输入数据
	scanf("%d", &N);
	for(int i = 1; i <= N; i++) {
		scanf("%d", &num[i]);//计算数组前缀和
		num[i] = num[i] + num[i - 1];
	}
	
	int ans = num[1]; //ans保存最大子数列和,初始化为num[1]能保证最终结果正确
	//i和j分别是枚举的子数列的起点和终点
	for(int i = 1; i <= N; i++) {
		for(int j = i; j <= N; j++) {
			int s = num[j] - num[i - 1];
			if(s > ans) ans = s;
		}
	}
	printf("%d\n", ans);

	return 0;
}

2.分治算法优化时间复杂度 O(N*lg N)

分而治之,即利用函数的递归调用来解决此问题
用一个函数,将一个数组以中心位置为分割点,分两个部分,答案可以分为三种情况:
1.最大子数列在左边
2.在右边
3.横跨分割点,左边右边各占一部分。

遇到1,2种情况,可以递归,直到出现第三种情况为止。
处理第3种情况:
1.以分割点为起点,分别找出左边和右边的最大值(由于确定了的起点,所以复杂度不高),相加即可。
思路如此清晰,那么我们要做的是只是判断是哪种情况即可,这也并非问题,可以继续用递归调用求出1、2的最大子数列,然后与分割点左右两边的最大值比较即可

int solve(int left,int right,int *num) {//左值,右值,数组地址 
	if(left==right) {//递归结束 
		return num[right];
	}
	int mid=right+left>>1;
	int lans =solve (right,mid,num);//Left Answer,即left max 
	int rans = solve (mid+1,left,num);//Right Answer

	int sum = 0, lmax = num[mid], rmax = num[mid + 1];
	for(int i = mid; i >= left; i--) {
		sum += num[i];
		if(sum > lmax) lmax = sum;
	}
	sum = 0;
	for(int i = mid + 1; i <= right; i++) {
		sum += num[i];
		if(sum > rmax) rmax = sum;
	}

	//答案是三种情况的最大值
	int ans = lmax + rmax;
	if(lans > ans) ans = lans;
	if(rans > ans) ans = rans;

	return ans;
}

3.动态规划优化时间复杂度 O(N)

在学习动态规划之后,最大子数列和问题的时间复杂度得到了有效的优化。

假设数组f[i]表示带有第i项的最大子数列,遍历一遍f[i]即可找到答案。

通过观察可知:

f[i] = max( f[i-1] , 0) + a[i] ;

对于f[i]的前项f[i-1],f[i]只有取和不取的两种选择,那么选大的就行。

依照这个思路,我们就可以写出以下代码:

#include<iostream>
using namespace std;
int a[1<<16] , ans[1<<16] ;
int main(){
	int n , maxn = -(1<<30);
	cin >> n;
    //读取
	for(int i = 1 ; i <= n ; i++)
		cin >> a[i];
    //构造含有a[i]的最大子数列ans[i],同时寻找最大值
	for(int i = 1 ; i <= n ; i++){
		ans[i] = max(ans[i-1] , 0 ) + a[i];
		maxn = max( maxn , ans[i]); 
	}
	cout << maxn <<endl;
	return 0 ;
}

时间复杂度成功优化到了O(N)!让我们进行下一…稍等,好像还能稍稍优化一下,读取步骤和构造步骤似乎能同时进行,没有影响:

#include<iostream>
using namespace std;
int a[1<<16] , ans[1<<16] ;
int main(){
	int n , maxn = -(1<<30);
	cin >> n;
	for(int i = 1 ; i <= n ; i++){
		cin >> a[i];
		ans[i] = max(ans[i-1] , 0 ) + a[i];
		maxn = max( maxn , ans[i]); 
	}
	cout << maxn <<endl;
	return 0 ;
}

好!做了个小优化,很舒服!

4.优化空间复杂度O(1)

在以下递推表达式中

f[i] = max( f[i-1] , 0) + a[i] ;

真正能同时用到的变量只有f[i],f[i-1],和a[i],其他变量都是用一次就丢弃了

所以,我们只需要设少量变量交替使用就可以了:

#include<iostream>
using namespace std;
int main(){
	int n , now , sum ,ans;
    /*
    n 数字个数
    now 当前的数字
    sum 带上当前数字的最大子数列
    ans 准备输出的答案
    */
	cin >> n;
	cin >> now;
	sum = ans = now;//必须先读取一个now赋上初值
	for(int i = 1 ; i < n ; i++)
	{
		cin >> now;
		sum = max(sum , 0) + now;
		ans = max(sum , ans);
	}
	cout << ans ; 
} 

从时间空间双O(N²)优化到了时间O(N),空间O(1),算法真是太有魅力了!
本篇博客于4月24日补充

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值