最大子数组:分而治之

Posted

技术标签:

【中文标题】最大子数组:分而治之【英文标题】:Maximum Subarray: Divide and Conquer 【发布时间】:2013-01-16 07:24:29 【问题描述】:

免责声明:这是一个作业。我不是要求明确的代码,只要求足够的帮助来理解所涉及的算法,以便我可以修复代码中的错误。

好的,所以您可能熟悉最大子数组问题:计算并返回数组中最大的连续整数块。很简单,但这个任务需要我以三种不同的复杂度来完成:O(n^3)、O(n^2) 和 O(n log n)。我已经得到了前两个没有太多麻烦(蛮力),但第三个让我头疼..字面意思。

我理解算法应该如何工作:一个数组被传递给一个函数,该函数递归地将它分成两半,然后比较各个组件以找到每一半的最大子数组。然后,因为最大子数组必须完全位于左半部分或右半部分,或者位于与两者重叠的段中,所以您必须找到与左半部分和右半部分重叠的最大子数组。比较每种情况的最大子数组,最大的将是您的返回值。

我相信我编写的代码可以充分执行该任务,但我的评估似乎是错误的。我一直在尝试联系导师和助教寻求帮助,但我觉得他们中的任何一个都没有取得任何进展。以下是我迄今为止设法编写的代码。如果您发现任何明显的错误,请告诉我。同样,我不是在寻找明确的代码或答案,而是帮助理解我做错了什么。我浏览了这里介绍的所有类似案例,但没有发现任何可以真正帮助我的东西。我也做了很多谷歌搜索以寻求指导,但这也无济于事。无论如何,这是有问题的代码:

int conquer(int arr[], int first, int mid, int last) 

    int i = 0;
    int maxLeft = 0;
    int maxRight = 0;

    int temp = 0;
    for (i = mid; i >= first; i--) 
        temp = temp + arr[i];
        if (maxLeft < temp) 
            maxLeft = temp;
        
    
    temp = 0;
    for (i = (mid + 1); i <= last; i++) 
        temp = temp + arr[i];
        if (maxRight < temp) 
            maxRight = temp;
        
    

    return (maxLeft + maxRight);


int divide(int arr[], int start, int end) 

    int i;
    int maxSum;
    int maxLeftSum;
    int maxRightSum;
    int maxOverlapSum;

    if (start == end) 
        return arr[start];
     else 
        int first = start;
        int mid = (end / 2);
        int last = end;

        maxSum = 0;
        maxLeftSum = 0;

        for (i = first; i < mid; i++) 
            maxSum = maxSum + arr[i];
            if (maxLeftSum < maxSum) 
                maxLeftSum = maxSum;
            
        
        for (i = (mid + 1); i < last; i++) 
            maxSum = maxSum + arr[i];
            if (maxRightSum < maxSum) 
                maxRightSum = maxSum;
            
        

        maxOverlapSum = conquer(arr, first, mid, last);
    

    if ((maxLeftSum > maxRightSum) && (maxLeftSum > maxOverlapSum)) 
        return maxLeftSum;
     else if ((maxRightSum > maxLeftSum) && (maxRightSum > maxOverlapSum)) 
        return maxRightSum;
     else
        return maxOverlapSum;

编辑:我得到的错误是不正确的结果。我的其他两种算法之间的结果一致且正确,但这个不正确。

编辑#2:这是我的代码的更新版本,精简了一点,我修复了一些问题。它仍然无法正常运行,但应该更接近......

#include <stdio.h>
#include <stdlib.h>

int numarr[] = 22, -27, 38, -34, 49, 40, 13, -44, -13, 28, 46, 7, -26, 42,
        29, 0, -6, 35, 23, -37, 10, 12, -2, 18, -12, -49, -10, 37, -5, 17,
        6, -11, -22, -17, -50, -40, 44, 14, -41, 19, -15, 45, -23, 48, -1,
        -39, -46, 15, 3, -32, -29, -48, -19, 27, -33, -8, 11, 21, -43, 24,
        5, 34, -36, -9, 16, -31, -7, -24, -47, -14, -16, -18, 39, -30, 33,
        -45, -38, 41, -3, 4, -25, 20, -35, 32, 26, 47, 2, -4, 8, 9, 31, -28,
        36, 1, -21, 30, 43, 25, -20, -42;

int length = ((sizeof(numarr))/(sizeof(int)));

int divide(int left, int right) 

    int mid, i, temp, mLeft, mRight, mCross = 0;

    if (left == right) 
        return left;
     else if (left > right) 
        return 0;
     else 
        mid = (left + right) / 2;

        divide(left, mid);
        divide(mid + 1, right);

        for (i = mid; i >= left; i--) 
            temp = temp + numarr[i];
            if (mLeft < temp) 
                mLeft = temp;
            
        

        for (i = mid + 1; i <= right; i++) 
            temp = temp + numarr[i];
            if (mRight < temp) 
                mRight = temp;
            
        

        mCross = (mLeft + mRight);

        printf("mLeft:  %d\n", mLeft);
        printf("mRight: %d\n", mRight);
        printf("mCross: %d\n", mCross);

        return 0;
    


int main(int argc, char const *argv[])

    divide(0, length);
    return 0;

【问题讨论】:

我希望我有时间回答这个问题,但我即将开始一个多小时的通勤时间。我不得不提一下,这是很长时间以来我在 SO 上看到的最好的 presented 家庭作业任务之一。哦,与大多数学生不同,您的代码格式、命名等是专业的。它在没有 cmets 的情况下可读,这说明了很多。祝你好运。 您必须执行除法步骤。您当前的实现仅计算最终迭代的重叠和,因此您错过了较小子数组的重叠和。 我假设他们为您提供了答案,如果是这样,我很好奇他们声称它是什么。这将有助于了解您的算法何时最终达到所有气缸。 我不知道他们是否会,但我会在我弄清楚时告诉你。我几乎可以肯定我现在拥有该算法,因此只需清理我的实现以使其正常工作。不过,我已经接近了! @idigyourpast 我有 223 个答案,但如果来自...显示分区是......挑战 =P 【参考方案1】:

我仍然盯着你的问题,但我几乎立即发现了几个错误。

首先,如果firstlast 与它们的名字所指的一样,那么您会错误地找到中点。你这样做:

mid = end/2;

什么时候应该是这样的:

mid = first + (last-first)/2;

接下来,您的第一个枚举循环从[first,mid) 运行(注意右侧排除了mid)。这个循环包含arr[mid]元素:

    for (i = first; i < mid; i++) 

您的第二次从[mid+1,last) 运行,其中也不包括arr[mid] 元素:

    for (i = (mid + 1); i < last; i++) 

这会留下一个元素的洞,特别是arr[mid]。现在,我并没有声称我完全理解该算法,因为我几乎没有机会阅读它,但如果您打算覆盖[first,last) 的整个范围,那么这可能不会这样做。此外,由 SauceMaster 链接的论文提出的教科书算法的明显缺点是使用一种语言,该语言不允许您偏移到数组中并通过指针衰减将其传递给函数调用作为基地址大批。 C 允许你这样做,你应该利用它。我想你会发现它使数字更容易理解,并且不需要你传入的索引之一。

例如:一个接受数组、中间分割和递归的函数可能看起来像这样:

void midsplit( int arr[], int len)

    if (len < 2)
    
         // base case
    
    else
    
        int mid = len/2;
        midsplit(arr, mid);
        midsplit(arr+mid, len-mid);

        // cumulative case
     

在每次递归中,分割点始终是一个范围的结尾,并用于对第二个范围进行偏移寻址,在递归调用中将其视为自己的从 0 开始的数组。不知道你是否可以使用它,但它确实让它更容易掌握(至少对我来说)。

最后,我可以看到,您的除法似乎没有做太多递归,这将是一个问题,因为这毕竟是一种递归算法。看来您至少错过了一个对divide() 的呼叫。

我可能错过了一些东西,这不是第一次,但正如我所说,我并没有倾注太多(目前)。

【讨论】:

嘿,我只想说谢谢你在回复中付出了这么多的思考和努力。我不能说我完全理解了它,但我正在一点一点地理解它。让这里的人愿意帮助像我这样的新学生真的很有帮助,而不是一堆侮辱或挫折。所以,是的,谢谢你:)【参考方案2】:

John Bentley 于 1984 年就此发表了一篇论文。您可以在线免费找到 PDF:http://www.akira.ruc.dk/~keld/teaching/algoritmedesign_f03/Artikler/05/Bentley84.pdf

他在第二页开始讨论 O(n log n) 方法。

【讨论】:

这太好了,谢谢。当我不忙于作业时,我将不得不花更多时间阅读这篇文章!【参考方案3】:

我认为您几乎拥有所需的所有代码,但是这两个问题对我来说很突出:

mid 变量计算存在问题。 您的 divide 函数实际上并没有进行任何除法。

在分治的递归公式中,您将在数组的下半部分递归调用除法函数,然后在数组的上半部分调用。征服步骤可以被认为是计算重叠和并返回三个候选者中最大者的代码。

编辑:在递归思考问题时,实现函数的目的,并加以利用。在这种情况下,divide 函数将返回所提供数组的最大子数组总和。因此,计算maxLeftSum的方法是在左子数组上调用除法。 maxRightSum 也是如此。

int divide(int arr[], int start, int end) 
    if (end > start) 
        int mid = (start + end)/2;
        int maxLeftSum = divide(arr, start, mid);
        int maxRightSum = divide(arr, mid+1, end);
        return conquer(arr, start, end, maxLeftSum, maxRightSum);
    
    return (start == end) ? arr[start] : 0;

祝你好运!

【讨论】:

您对mid 的计算是正确的。根据 WhozCraig 的建议,我已将其更改为:-mid = (start + end / 2) 关于divide 函数,我仍然很难递归地考虑这个问题。我最初有递归调用,但不小心删除了它们并且没有意识到它们已经消失了。也就是说,我不太确定把它们放在哪里。如果我把它们放在循环之前,循环仍然有效吗?如果我把它们放在循环之后,它们就没用了,因为循环已经遍历了整个数组段。【参考方案4】:

我认为你只关注交叉子阵部分。但是,也有左子阵部分和右子阵部分,它们都有比交叉子阵更大的可能性。

因为英语不是我的第一语言而且我不擅长它。我不相信我已经表达了我想要表达的确切内容,所以我将粘贴我的代码。 这是我的代码:

int find_max_subarr(int a[],int l,int r)

    if(l>r)return 0;
    if(l==r) return a[l];
    int lsum=-1000,rsum=-1000;
    int sum=0;
    if(l<r) 
        int mid=(l+r)/2;
        for(int i=mid;i>=l;i--) 
            sum+=a[i];
            if(lsum<sum)lsum=sum;
        
        sum=0;
        for(int i=mid+1;i<=r;i++) 
            sum+=a[i];
            if(rsum<sum)rsum=sum;
        
        int all_sum=lsum+rsum;
        int llsum=find_max_subarr(a,l,mid);
        int rrsum=find_max_subarr(a,mid+1,r);
        if(llsum<all_sum&&rrsum<all_sum) return all_sum;
        if(all_sum<llsum&&rrsum<llsum)return llsum;
        if(all_sum<rrsum&&llsum<rrsum)return rrsum;
    


int main()

    int a[SUM]=100,113,110,85,105,102,86,63,81,101,94,106,101,79,94,90,97;
    int b[SUM-1];
    int t=0;
    for(int i=1;i<SUM;i++) 
        b[t]=a[i]-a[i-1];
        t++;
    
    int sum=find_max_subarr(b,0,t-1);
    cout<<sum<<endl;
    return 0;

【讨论】:

以上是关于最大子数组:分而治之的主要内容,如果未能解决你的问题,请参考以下文章

运行分而治之算法后从数组中打印最大子数组值

使用分而治之的最大子数组

用于查找最大子数组的分而治之算法 - 如何同时提供结果子数组索引?

如何使用分而治之的方法解决“固定大小的最大子数组”?

最大子数组的特殊情况

使用分而治之的最大子阵列产品有人吗?