算法导论--最大子数组问题

682 阅读5分钟

由股票问题,到最大子数组问题,再通过分治求解

问题描述

给出股票在一段时间内,每日的价格,你可以选择在一天买入,之后一天卖出,求可以获得的最大利润

求解

暴力: 遍历第i天买入,第j(j>i)天卖出的收益,求最大值

时间复杂度: O(n^2)

问题转换

定义一个数组A,A[i]为第i天卖出,i-1天买入,所获得的差值

	         0   1   2    3  4   5  6    7  8   9  10  11  12 13  14 15 16
prices: 	[100 113 110  85 105 102 86  63  81 101 94 106 101 79  94 90 97]
价格变化	[	  13  -3 -25 20  -3  -16 -23 18 20  -7 12  -5  -22 15 -4 7 ]
                                               |             |

问题转换为:寻找A的和最大的非空连续子数组: (在第7天之后买入,读11天后卖出,收益最大43)

用分治法求最大子数组问题

对于A[low...high]数组,将子数组划分为两个规模尽量相等的子数组,即找到数组中间位置,mid A[low...high]的任何连续子数组A[i..j]所处的位置必然是以下三种情况之一:

  1. 完全位于左边A[low..mid], low<=i<=j<=mid
  2. 完全位于右边A[mid+1..high], mid+1<=i<=j<=high
  3. 跨越了中点:low<=i<=mid<j<=high

前两个子问题是最大子数组问题的规模更小的子问题,剩下个跨越中点的情况,在三者之中求最大值:

可以在线性时间内求出跨越中点的最大子数组,因为它存在限制条件,该子数组必须跨越中点。

任何跨越中点的子数组都由两个子数组组成:A[i...mid]和A[mid+1..j], low<=i<=mid, mid<j<=high

我们只需要找出A[i..mid]和A[mid+1..j]的最大子数组,然后合并即可

伪代码

FIND_MAX_CROSSING_SUBARRY(A, low, mid, high)
	left-sum=-MAX
	sum=0
	for i=mid downto low 
		sum = sum+A[i]
		if sum>left-sum
			left-sum = sum 
			max-left=i 
	right-sum=-MAX 
	sum=0
	for j=mid+1 to high 
		sum=sum+A[j]
		if sum>right-sum
			right-sum=sum 
			max-right=j
	return(max-left, max-right, left-sum+right-sum)

最大子数组的分治算法

伪代码

FIND_MAXIMUM-SUBARRY(A, low, high)
	if low==high
		return(low, high, A[low])
	else mid=(low+high)/2
		(left-low,left-high,left-sum) = FIND_MAXIMUM-SUBARRY(A,low,mid)
		(right-low, right-high, right-sum) = FIND_MAXIMUM-SUBARRY(A, mid+1, high)
		(cross-low, cross-high, cross-sum) = FIND_MAX_CROSSING_SUBARRY(A, low, mid, high)
		if left-sum>=right-sum and left-sum>=cross-sum
			return (left-low, left-high, left-sum)
		elif right-sum>=left-sum and right-sum>=cross-sum
			return (right-low, right-high, right-sum)
		else 
			return (cross-low, cross-high, cross-sum)

时间复杂度:

n==1: T(n) = O(1)

n>1: T(n) = 2T(n/2)+O(n);

平均时间复杂度O(nlgn)

C代码

#include <stdio.h>
#include <rlimit.h>

void find_max_cross_subarray(int *nums, int low, int mid, int high, int* cross_low, int* cross_high, int*cross_sum){
	int left_sum = INT_MIN;
	int sum=0;
	int i;
	int max_left = mid;
	for(i=mid; i>=low; i--){
		sum += nums[i];
		if(sum>left_sum){
			left_sum = sum;
			max_left = i;
		}
	}
	int right_sum=0;
	int max_right=mid+1;
	int j;
	sum=0;
	for(j=mid+1; j<=high;j++){
		sum+=nums[j];
		if(sum>right_sum){
			right_sum = sum;
			max_right = j;
		}
	}
	*cross_low = max_left;
	*cross_high = max_right;
	*cross_sum = left_sum+right_sum;
	return;
}

void find_maximum_subarry(int *nums, int low, int high, int *low_idx, int *high_idx, int *ans){
	if(high==low){
		*low_idx = low;
		*high_idx = high;
		*ans = nums[low];
		return;
	}
	else{
		int mid = (low+high)/2;
		int left_low,left_high,left_sum;
		find_maximum_subarry(nums, low,mid,&left_low,&left_high,&left_sum);
		
		int right_low, right_high, right_sum;
		find_maximum_subarry(nums, mid+1, high, &right_low, &right_high, &right_sum);
		
		int cross_low, cross_high, cross_sum;
		find_max_cross_subarray(nums,low, mid,high,&cross_low, &cross_high, &cross_sum);
		
		if(left_sum>=right_sum && left_sum>=cross_sum){
			*ans = left_sum;
			*low_idx = left_low;
			*high_idx = left_high;
		}
		else if(right_sum>=left_sum && right_sum>=cross_sum){
			*ans = right_sum;
			*low_idx = right_low;
			*high_idx = right_high;
		}
		else{
			*ans = cross_sum;
			*low_idx = cross_low;
			*high_idx = cross_high;
		}
		return;
	}
}

int main(){
	//int nums[] = {100, 113, 110,  85, 105, 102, 86,  63,  81, 101, 94, 106, 101, 79,  94, 90, 97};
	int nums[] = {13,  -3, -25, 20,  -3,  -16, -23, 18, 20,  -7, 12,  -5,  -22, 15, -4, 7};
	int ans, ans_low, ans_high;
	int len = sizeof(nums)/sizeof(int);
	find_maximum_subarry(nums, 0, len-1, &ans_low, &ans_high, &ans);
	
	printf("low: %d, high: %d, max_profit: %d \n", ans_low, ans_high, ans);
	return 0;
	
}

更优解

非递归,线性时间算法

描述: 从数组的左边界开始,从左至右处理,记录到目前为止已经处理过的最大子数组。 若已知A[1..j]的最大子数组,基于如下性质将解扩展为A[1..j+1]的最大子数组。 A[1..j+1]的最大子数组,要么是A[1..j]的最大子数组;要么是某个子数组A[i..j+1] 在已知A[1..j]的最大子数组情况下,可以在线性时间内找出A[i..j+1]的最大子数组

leetcode: 最大子数组和

C代码:

//[-2,1,-3,4,-1,2,1,-5,4]
//  ^ ^
//思路:从左到右,对每个i, 当前最大子数组和为dp[i-1],或dp[i-1](包含nums[i-1])+nums[i] 
//对i=0, 最大是0; i=1时最大是1;i=2时,最大是1
//包含nums[i]的最大和, 不包含Nums[i]的最大和 
//dp1(包含nums[i]):max(dp2[i-1]+nums[i], nums[i]):		-2 1
//dp2(不包含nums[i]: max(dp2[i-1], dp1[i-1])):	0
int max2(int x, int y){
	int a = x>y?x:y;
    return a;
}  
int maxSubArray(int* nums, int numsSize) {
    int *dp1 = malloc(sizeof(int)*numsSize); //包含Nums[i]
	int *dp2 = malloc(sizeof(int)*numsSize); //不包含nums[i]
	//if(numsSize<=1)	return nums[0];
	dp1[0] = nums[0];
	dp2[0] = 0;
	int ans = nums[0];
    int flag=-1;
	for(int i=1; i<numsSize;i++){
		dp1[i] = max2(dp1[i-1]+nums[i], nums[i]);
		dp2[i] = max2(dp2[i-1], dp1[i-1]); //如果每次都不包含的话,取数组中的最大值
        //printf("%d %d %d \n", i, dp1[i], dp2[i]);
        if(dp2[i]>dp1[i]){
            ans = max2(ans, max2(dp1[i], dp2[i]));
        }else{
            flag=1;
            ans = max2(ans, max2(dp1[i], dp2[i]));
        }
	}
    if(flag==-1){
        //和一直是递减的(全为负数)
        ans = nums[0];
        for(int i=1; i<numsSize; i++){
            ans = max2(ans, nums[i]);
        }
    }
	return ans;
}