动态规划——最大子段和问题

本文探讨如何使用动态规划求解最大子段和问题。通过分析三种可能的情况,包括子段和位于左半边、右半边以及跨越两边,详细解释了递归和动态规划算法的实现。动态规划方法中,通过s[i]表示以a[i]为结尾的最大子段和,并根据s[i-1]的状态决定s[i]的计算方式,最终找到最大值。
给定n个整数(可能为负数)组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的子段和的最大值。当所给的整均为负数时定义子段和为0,依此定义,所求的最优值为:
  
    Max{0,a[i]+a[i+1]+…+a[j]},1<=i<=j<=n

    例如,当(a1,a2,a3,a4,a4,a6)=(-2,11,-4,13,-5,-2)时,最大子段和为20。


简单算法:

/*
** 简单算法
** 两个for循环,需要O(n^2)的时间
** 空间就存储一个数组,但不是最优
*/
#include<stdio.h>
// besti,bestj记录最大段和时的头尾
// 元素存储在a数组中,a[0]...a[n-1]
int MaxSum(int n, int a[], int &besti, int &bestj) {
	int i , j , k;
	int sum = 0 , thissum;
	// 分别计算以每个元素为起始位置求解最大值
	for(i=0 ; i<n ; i++) {
		thissum = 0;
		// 计算以a[i]为起始元素
		// a[k]为结尾,k从i...n-1,记录下最大值
		for(j=i ; j<n ; j++) {
			thissum += a[j];
			// 当前值如果大于已求最大值,则存储
			if(thissum > sum) {
				sum = thissum;
				besti = i;
				bestj = j;
			}
		}
	}
	return sum;
}

int main() {
	int n , a[100];
	int i , besti , bestj;
	int sum;
	printf("请输入元素的个数:\n");
	scanf("%d",&n);
	printf("请输入各个元素:\n");
	for(i=0 ; i<n ; i++) {
		scanf("%d" , &a[i]);
	}
	 
	sum = MaxSum(n , a , besti , bestj);
	printf("最大字段和为:\n");
	
	for(i=besti ; i<bestj ; i++) {
		printf("%d + " , a[i]);
	}
	printf("%d = %d\n" , a[bestj] , sum);
	return 0;
}



使用分治法: 


如果将给定的序列a[1...n]分成两段长度相近的子段,分别为a[1...n/2] 和 a[n/2+1 ... n] , 试分别求出这两个子段的最大子段和,

则a[1... n]的最大子段和的情形有如下三种:

(1)a[1...n]的最大子段和与a[1...n/2]的最大子段和相同,也就是最大子段和在左半边

(2)a[1...n]的最大子段和与a[n/2+1...n]的最大子段和相同,也就是最大子段和在右半边

(3) a[1...n]的最大子段和跨越两边,和为a[i] + a[i+1] + ... + a[j] , 期中 1<= i <= n/2 ,  n/2+1<= j <= n

其中(1)、(2)两种情形可以用递归求得,而对于第(3)中则可以通过分别计算s1 , s2 

而 s1 为从中间位置a[n/2] 往前加求得最大值

而 s2为从中间位置a[n/2+1]往后加求的最大值

然后 s1 + s2 , 然后比较三种情形下的最大值,求出最大的即为最大子段和


#include<stdio.h>
/*
** 使用分治法
** 时间为O(nlogn)
*/
int MaxSumtwo(int left, int right, int a[]) {
	int sum = 0 , i , j;
	int center , leftsum , rightsum;
	int s1 , s2;
	int lefts , rights;
	// 递归出口
	if(left == right) {
		sum = a[left]>0 ? a[left]:0;
	} else {
		center = (left + right) / 2;
		// 前半段
		leftsum = MaxSumtwo(left,center,a);
		// 后半段
		rightsum = MaxSumtwo(center+1,right,a);
		// 分别计算s1 和 s2
		s1 = 0 ; lefts = 0;
		for(i=center ; i>=left ; i--) {
			lefts += a[i];
			if(lefts > s1) {
				s1 = lefts;
			}
		}

		s2 = 0 ; rights = 0;
		for(j=center+1 ; j<=right ; j++) {
			rights += a[j];
			if(rights >= s2) {
				s2 = rights;
			}
		}
		// 三种情况下的sum值进行比较
		sum = s1 + s2;
		if(leftsum > sum) {
			sum = leftsum;
		}
		if(rightsum > sum) {
			sum = rightsum;
		}
	}
	return sum;
}

int main() {
	int n , a[100];
	int i;
	int sum;
	printf("请输入元素的个数:\n");
	scanf("%d",&n);
	printf("请输入各个元素:\n");
	for(i=0 ; i<n ; i++) {
		scanf("%d" , &a[i]);
	}

	sum = MaxSumtwo(0 , n-1 , a);

	printf("分治法求得的最大子段和为:%d\n" , sum);

	return 0;
}



动态规划算法:

找出数组的最大子段和。(即 max (a[i]+a[i+1]+...+a[j]) 0<=i<=j<=n-1)

现在我们的思路是: 记s[i]为  a[0]到a[i]中包含a[i] (即以a[i]为结尾)的最大子段和,则s[i]如何用s[i-1]来表示?

s[i] = s[i-1] + a[i];  s[i-1]>=0 或者

s[i] = a[i]; s[i-1]<0

所以根据s[i-1]来计算s[i]时会遇到这两种情况。计算s[i]时只用到s[i-1]和a[i], 所以我们可以只保留s[i-1]的值,对于s[i-2]及以前的值可以不保存。

在s[i]中找最大的,就是我们的结果了。

那究竟哪一段是最大的呢。这个方法不唯一。比如,你可以用数组记录每一个s[i],那如果s[i] = a[i]的时候,说明a[i-1]不在s[i]的计算结果中,a[i-1]就没被选择。

现在我们用一个辅助数组P来记录一下, p[i] = 1 表示 在计算s[i]时 包含了 p[i-1]; p[i] = 0 表示 p[i-1]没有被包含。

#include<stdio.h>
/*
** 使用动态规划
** 时间为O(n)
*/
int MaxSumthree(int n , int a[]) {
	int sum , thissum;
	int i;
	sum = 0;
	thissum = 0;
	for(i=0 ; i<n ; i++) {
		thissum += a[i];
		if(thissum < 0 ) {
			thissum = 0;
		}
		if(thissum > sum) {
			sum = thissum;
		}
	}
	return sum;
}

int main() {
	int n , a[100];
	int i;
	int sum;
	printf("请输入元素的个数:\n");
	scanf("%d",&n);
	printf("请输入各个元素:\n");
	for(i=0 ; i<n ; i++) {
		scanf("%d" , &a[i]);
	}

	sum = MaxSumthree(n , a);

	printf("分治法求得的最大子段和为:%d\n" , sum);

	return 0;
}





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值