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

本文介绍了三种不同的算法来寻找数组中的最大子数组之和,包括暴力求解、分治法及线性时间复杂度的方法,并详细解释了线性时间实现的原理与证明过程。此外,还提供了一种求解最大子数组乘积的方法。

方法一:
暴力求解

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int maxsubset(int *a,int len){
    int summax=INT_MIN;
    int i,j,k;
    for(i=0;i<len;i++)
        for(j=i;j<len;j++){
            int temp=0;
            for(k=i;k<=j;k++)
                temp+=a[k];
            if(temp > summax) summax=temp;
        }
    return summax;
}
int main(){
    int a[]={3,-1,2,5,-3,4,-6,-7,1,8,-3,5,9};
    printf("the maxsubset:%d\n",maxsubset(a,13));
    return 0;
}

方法二:分治求解

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int cross_middle(int *a,int l,int m,int r){
    int i,
        sum=0,
        l_max=INT_MIN,
        r_max=INT_MIN;
    for(i=m;i>=l;i--){
        sum+=a[i];
        if(sum > l_max) l_max=sum;
    }
    sum=0;
    for(i=m+1;i<=r;i++){//最早i=m,出现BUG
        sum+=a[i];
        if(sum>r_max) r_max=sum;
    }
    return (l_max+r_max);
}
int maxsubset(int *a,int l,int r){
    if(l == r) return a[l];
    //if(l>r) return 0;

    int m=(l+r)/2,
        l_max=INT_MIN,
        r_max=INT_MIN,
        c_max=INT_MIN;
    l_max=maxsubset(a,l,m);
    r_max=maxsubset(a,m+1,r);
    c_max=cross_middle(a,l,m,r);
    if(l_max >= r_max&&l_max >= c_max) return l_max;
    else if(r_max >= l_max&&r_max >= c_max) return r_max;
    else return c_max;
}
int main(){
    int a[]={3,-1,2,5,-3,4,-6,-7,1,8,-3,5,9};

    printf("the maxsubset:%d\n",maxsubset(a,0,12));
    return 0;
}

方法三 线性时间实现:

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int maxsubset(int *a,int l,int r){
    int i,
        temp=0,
        summax=INT_MIN;
    for(i=l;i<=r;i++){
        temp+=a[i];
        if(temp > summax) summax=temp;
        if(temp < 0) temp=0;
    }
    return summax;
}
int main(){
    int a[]={3,-1,2,5,-3,4,-6,-7,1,8,-3,5,9};

    printf("the maxsubset:%d\n",maxsubset(a,0,12));
    return 0;
}

方法三线性时间实现基于这样的思路:
如果a[1..j]已经得到了其最大子数组,那么a[1..j+1]最大子数组只能是两种情况
(1)a[1..j+1]的最大子数组就是a[1..j];
(2)a[1..j+1]的最大子数组是a[i..j+1],1<=i<=j;
那么,如何求得所谓的(2)中的a[i..j+1]呢?
首先需要承认这样的事实,如果一个数组a[p..r]求和得到负值,那么数组a[p..r+1]的最大子数组肯定不会是a[p..r+1],因为a[p..r]+a[r+1]<a[r+1].
在以上程序中,我们用temp存储所谓的a[p..r],只要a[p..r]的求和是负值,那么从下一个a[r+1]值开始,temp重新从零开始求和,只要temp > summax,就更新summax,这样,我们一次遍历后,就能得到最大的summax,接下来,我们证明该算法是有效的
证明:
对于所有数组元素,这样的元素对数组进行划分,如果加上该元素之前temp>0且temp+a[i]<0,那么该元素a[i]是一个边界,这样,数组会形成好多段,每段结束元素都满足temp>0且temp+a[i]<0.所以我们能得到多个划分块a[p..q],每个划分快的和是负值,划分块有这样的性质,对任意p<=i<q,显然,sum(a[p..i])>=0且sum(a[i..q])<0;
我们要证明

(1)最大子数组一定在划分块之内
证明:
根据划分快性质,容易证明,只要子数组横跨多个划分快,其求和值必定小于某个单独的划 分快中的数组求和。

(2)一定存在首元素以划分块的首元素开始的最大子数组。

证明:
对于某个划分快a[p..q],假设存在a[i..j],其中p<i<=j<q,那么根据划分块性质,a[p..i-1]+a[i..j]>=a[i..j]必定成立,得证。
所以,经历一次遍历,对于每个划分块,从首元素开始求和,得到最大值更新summax,一定能够得到最大子数组的值。

//-------------------------------------------------------------------------
求数组的最大子数组的积怎么求呢?
解决方法是保存最大负值和最大正值,实现如下:

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int sunsetmax_mul(int *a,int l,int r){
    int i,temp,
        nmax=0,//negtive max
        pmax=0,//positive max
        _max=a[l];
        nmax=(_max<0 ? _max : 0);
        pmax=(_max>0 ? _max : 0);
    for(i=l+1;i<=r;i++){
        if(a[i] < 0){
            temp=pmax;
            pmax=nmax==0 ?  0 : nmax*a[i];
            nmax=temp==0 ?a[i]: temp*a[i];
        }
        else if(a[i] > 0){
            pmax=pmax==0 ? a[i]: pmax*a[i];
            nmax=nmax==0 ?   0 : nmax*a[i];
        }
        else {
            pmax=nmax=0;
        }
        if(pmax > _max) _max=pmax;
    }
    return _max;
}
int main(){
    int a[]={-2,2,2,-5,2/*,5,-3,4,-6,-7,1,8,-3,5,9*/};
    printf("max mul subset result is %d\n",sunsetmax_mul(a,0,4));
}

其他思想:
另一种思路(包括最大子数组的积)

求不相交两个子数组的最大和

最大子数组是连续的若干数组元素,若其和是最大的,那么这个子数组就称为该数组的最大子数组,它是很多问题的抽象,比如购买股票,把相邻两天的股价之差作为数组元素,求在连续的某个时间段内买入股票的最佳时间和卖出股票的最佳时间就可以抽象为计算最大子数组问题 [^3]。 求解最大子数组问题有分治法和贪心算法(这里结合动态规划思想的变种贪心算法,即Kadane算法)等方法: - **分治法**:将数组 `X[1…n]` 分为 `X[1…n/2]` 和 `X[n/2 +1…n]`,递归求解子问题,包括数组 `X[1…n/2]` 的最大子数组 `S1`、数组 `X[n/2+1…n]` 的最大子数组 `S2`,然后计算跨中点的最大子数组 `S3`,数组 `X` 的最大子数组之和 `Smax = max{S1, S2, S3}`,时间复杂度为 $O(nlogn)$ [^5]。 ```python def find_max_crossing_subarray(arr, low, mid, high): left_sum = float('-inf') sum_val = 0 max_left = mid for i in range(mid, low - 1, -1): sum_val += arr[i] if sum_val > left_sum: left_sum = sum_val max_left = i right_sum = float('-inf') sum_val = 0 max_right = mid + 1 for j in range(mid + 1, high + 1): sum_val += arr[j] if sum_val > right_sum: right_sum = sum_val max_right = j return max_left, max_right, left_sum + right_sum def find_max_subarray(arr, low, high): if high == low: return low, high, arr[low] else: mid = (low + high) // 2 left_low, left_high, left_sum = find_max_subarray(arr, low, mid) right_low, right_high, right_sum = find_max_subarray(arr, mid + 1, high) cross_low, cross_high, cross_sum = find_max_crossing_subarray(arr, 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 arr = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7] low, high, max_sum = find_max_subarray(arr, 0, len(arr) - 1) print(f"最大子数组的起始下标: {low}, 结束下标: {high}, 最大和: {max_sum}") ``` - **Kadane算法**:从数组左边界开始,由左到右处理,记录到目前为止已处理过的最大子数组。若已知 `A[1...j]` 的最大子数组,`A[1...j+1]` 的最大子数组要么是 `A[1...j]` 的最大子数组,要么是某个子数组 `A[i...j+1]` 的最大子数组(`1 <= i <= j+1`),在已知 `A[1...j]` 的最大子数组的情况下,可以在线性时间内找出形如 `A[i...j+1]` 的最大子数组,时间复杂度为 $O(n)$ [^1]。 ```python def max_subarray_sum(arr): max_ending_here = max_so_far = arr[0] for num in arr[1:]: max_ending_here = max(num, max_ending_here + num) max_so_far = max(max_so_far, max_ending_here) return max_so_far arr = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7] print(f"最大子数组和: {max_subarray_sum(arr)}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值