由股票问题,到最大子数组问题,再通过分治求解
问题描述
给出股票在一段时间内,每日的价格,你可以选择在一天买入,之后一天卖出,求可以获得的最大利润
求解
暴力: 遍历第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]所处的位置必然是以下三种情况之一:
- 完全位于左边A[low..mid], low<=i<=j<=mid
- 完全位于右边A[mid+1..high], mid+1<=i<=j<=high
- 跨越了中点: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;
}