分治策略(最大子数组)

1 问题

最大子数组问题(问题来源算法导论)
  对于一个数组A,寻找A的和最大的非空连续子数组,称这样的连续子数组为最大子数组(maximum subarray)。当然A中可能有负数,不然A的最大子数组就是A本身。
  例如在以下数组中:

12345678
-16-231820-712-5-22

  最大子数组为下标3-6的子数组即[18 20 -7 12]其和为43。


2 解题思路

2.1 穷举法

  从左至右,找出所有以i(i=1 2…n)下标为初始下标的子数组,此举需要O(n)时间复杂度,每个以i为下标的子数组可以递增自身长度j次(j<n)此举需要O(n)时间复杂度;而这样的i有k个(k<n),所以总时间复杂度为O(n)*O(n)*O(n)=O(n^3)

2.2 分治策略

  先定义一个线性时间复杂度为O(n)的函数,用于找出包含“中间位置”的最大子数组。然后定义递归函数,递归地找出左端、右端的最大子数组,最后将递归得出的左端、右端最大子数与左端、右端合并的最大子数组进行比较,得出其较大值就为问题的解,而因为是对原数组折半递归,其递归树深度为 l o g 2 n log{_{2}}{n} log2n,所以其时间复杂度为O(lgn),因此总的时间复杂度为O(n)O(lgn)=O(nlgn)。

2.3 线性策略

  因为本文侧重通过分治策略解决问题,对于问题本身的线性更优解先不作讨论,线性方法详细如下:
                                             点此跳转

3 Matlab代码实现

3.1穷举法实现

A=[-16 -23 18 20 -7 12 -5 -22];
maxResult=0;                %maxResult保存最大子数组的和
for i=1:size(A,2)           %以下标i开始的所有子数组
    for j=i:size(A,2)       %子数组的长度每次递增1
        tmp=[];
        for k=i:j           %从i开始的子数组以j结束
            tmp=[tmp,A(k)];
        end
        if sum(tmp)>maxResult
            maxResult=sum(tmp);
            left_index=i;
            right_index=k;
        end
    end
end
disp(left_index);
disp(right_index);
disp(maxResult);

3.2.分治策略实现

find_maximum_subarray.m(递归定义自身找出最大子数组)
  为了下文讨论方便,下文将find_maximum_subarray() 简称为f1。

function [a,b,c]=find_maximum_subarray(A,low,high)
if high==low                                              %基线条件:数组中只有一个元素
    a=low;
    b=high;
    c=A(low);
    return
else
    mid=floor((low+high)/2);
    [left_low,left_high,left_sum]=find_maximum_subarray(A,low,mid);          %递归找出最大左子数组
    [right_low,right_high,right_sum]=find_maximum_subarray(A,mid+1,high);    %递归找出最大右子数组
    [cross_low,cross_high,cross_sum]=find_max_cross_subarray(A,low,mid,high);
    if left_sum>=right_sum && left_sum>=cross_sum         %最大子数组在左端取得
        a=left_low;
        b=left_high;
        c=left_sum;
        return
    elseif right_sum>=left_sum && right_sum>=cross_sum    %最大子数组在右端取得
        a=right_low;
        b=right_high;
        c=right_sum;
        return
    else                                                  %最大子数组在中间取得
        a=cross_low;
        b=cross_high;
        c=cross_sum;
        return
    end
end

find_max_cross_subarray.m(找出包含“中间位置”的最大子数组)
  为了下文讨论方便,下文将find_max_cross_subarray()简称为f2

%求出给定子数组跨越中线(mid)的最大连续和
%A为给定子数组
%low为下标
%mid为中线
%high为上标
%该函数的事件复杂度为O(n)
function [max_left,max_right,lrsum]=find_max_cross_subarray(A,low,mid,high)
%% 求出左半部分的最大子数组
left_sum=-10^(10);     %保存目前为止找到的最大和
sum=0;                 %保存A[i...mid]中所有值的和
i=mid;
while i>=low
    sum=sum+A(i);
    if sum>left_sum
        left_sum=sum;
        max_left=i;   %记录A[i...mid]中最大和的下标i
    end
    i=i-1;
end
%% 求出右半部分的最大子数组
right_sum=-10^(10);   %负无穷
sum=0;
for j=mid+1:high
    sum=sum+A(j);
    if sum>right_sum
        right_sum=sum;
        max_right=j;
    end
end
lrsum=left_sum+right_sum;

命令行调用以下命令输出结果

find_maximum_subarray(A,1,size(A,2))

输出结果:
在这里插入图片描述

4 讨论

递归树(右边子树部分没有表述全)
  数据用描述问题中的例示数据。 整个递归树的调用过程类似于满二叉树的先序遍历…
  由递归树也可以容易看出,分治策略的时间复杂度。因为递归树的深度为 l o g 2 n log{_{2}}{n} log2n,即一共有 l o g 2 n log{_{2}}{n} log2n层需要计算,而计算每层的时间复杂度都为O(n),因此总的时间复杂度为 O ( l o g 2 n ) O({log{_{2}}{n}}) O(log2n)*O(n)= O ( n ∗ l o g 2 n ) O({n*log{_{2}}{n}}) O(nlog2n)

在这里插入图片描述

在上图中,
   f1(A,low,high)指示find_maximum_subarray.m函数,用于找出在数组A的low到high范围内的最大子数组;开头调用f1(A,1,8)实际上就是想要求出原问题的解,而求解f1(A,1,8)则需要求解 f1(A,1,4), f2(A,1,4,8)、f1(A,4,8)三个的问题的解(分治策略),分别表示求出f(A,1,8)的左端最大子数组、跨越“中间”的左右端合并最大子数组、以及右端最大子数组,因此每层f1的调用都返回对应的3个值。简单地说,最大子数组肯定是当前数组的左端,右端或者是左右两端合并的的子数组中取得,递归求解就能得到最后的结果。
   f2(A,low,mid,high)指示find_max_cross_subarray.m函数,用于找出low到high范围内跨越mid的A中最大子数组
  f1、f2傍边的黑色圆圈里面的值表示该函数执行时的顺序号,1| f1(A,1,8)前的1表示这个函数最先被调用,而21 | f2(A,1,4,8)表示这个函数是第21个被调用的函数,在上例中也是最后一个被调用的函数。红色圆圈内的值为调用函数的返回值,红色矩形框为调用f2求出的最大子数组。

分治三部曲

  1. 分解(Divide),   将问题划分为一些子问题,子问题与原问题一样,只是规模更小;
  2. 解决(Conquer),  如果子问题的规模足够小,则停止递归,直接求解;
  3. 合并(Combine),  将子问题的解合成原问题的解;
      在本例中,求解f1(A,low,high)需要将问题分为f1(A,low,mid)、f1(A,mid+1,high),以及f2(A,low,mid,hgih)3部分,而前两部分f1(A,low,mid)、f1(A,mid+1,high)实际上与原问题一样,只是问题规模变小了可以进一步求解,体现了3部曲中的第1部——分解(Divide)
      而递归程序不能够永远执行下去,即需要有一个递归出口,本例的递归出口就是A的low==high时,即当前数组只有一个元素,因此最大子数组为其本身,直接求解,返回元素本身作为最大子数组,也就是3部曲中的第2部——解决(Conquer)
      在本例中,求出了数组左端的最大子数组、右端的最大子数组、以及包含中间的最大子数组后需要在这3个中选择出一个最优解,也就是分解子问题得到的解合并成为原问题的解——合并(Comnine)

分治与递归

  虽然采用分治策略编写的递归程序程序简洁,但是却不好理解,原因在于递归栈的调用过程都是隐式调用,给人一种看不出程序是如何执行的感觉,对此,可以多手动模拟递归栈的调用过程,加深理解。

动态规划与分治法

  适合用分治法求解的问题通常在递归的每一步都生成全新的子问题;动态规划因为具有重叠子问题性质,所以一直都是在反复求解相同的子问题,而不是一直生成新的子问题。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值