学习算法导论的时候,有一个买股票的例子,我觉得挺有意思的,书上是这么说的:假定你获得了投资挥发性化学公司的机会。与其生产的化学制品一样,这家公司的股票价格也是不稳定的。你被准许可以在某个时刻买进一股该公司的股票,并在之后某个日期将其卖出,买进卖出都是在当天交易结束后进行。为了补偿这一限制,你可以了解股票将来的价格。你的目标是最大化收益。图4-1给出了17天内的股票价格。第0天的股票价格是每股100美元,你可以在此之后任何时间买进股票。你当然希望“低价买进.高价卖出”—在最低价格时买进股票,之后在最高价格时卖出,这样可以最大化收益。但遗憾的是,在一段给定时期内,可能无法做到在最低价格时买进股票,然后在最高价格时卖出.例如,在图4-1中,最低价格发生在第7天,而最高价格发生在第1天—最高价在前,鼓低价在后。其实就是求最大连续子数组的问题,求在连续的买入股票,使得收益最大,当然是用暴力求解的方法也可以求出来,时间复制度为O(n^2)。不过有更好的方法,那就是分治法,时间复杂度为O(nlgn)
暴力解决:Python学习交流群:1004391443
#暴力求解最大子数组 def find_max_list(listn,n): #listn:要求解的数组 n:数组大小 sum=0 list_result=[listn[0],0,0] #结果为列表,存储和的最大值和结果子数组的位置 for i in range(n-1): #这里只循环到第listn[n-2],考虑到当i=n-1时j的取值 sum=listn[i] #i变动时,sum进入新的循环,重新赋值为listn[i] if sum>list_result[0]: list_result=[sum,i,i] for j in range(i+1,n,1): sum=sum+listn[j] if sum>list_result[0]: list_result=[sum,i,j] if list_result[0]
- 分:可以将数组分为两部分,要尽量均匀,所以最好是从中间分,结果可以能在前半数组,可能在后半数组,也可能跨前后数组
- 治:我们可以递归求解前半数组最大值,递归求解后半数组最大值,再求解跨前后数组最大值
- 合并:将求得的值返回,比较求解到的三个最大值,返回其中的最大值
在我的理解中,分治法就是将一个大问题以迭代的方式不断分解为小问题,直到最后分解的小问题的规模小到能够以很小的代价求解出来,再将这些小问题的解综合起来以得到开始的大问题的解。这里面两个难点,一是如何通过迭代将大问题分解成易于求解的小问题,即“分”的问题,二是如何将这些小问题的解综合或者说还原成大问题的解,即“治”。 “分”的话,很容易便想到将数组不断地对半分,直至最终分得的小问题规模只有一个元素,那么这个小问题的解也就是这个数字了,但问题在于“治”,这样的分法有一个问题,就是将n个数字分成1—n/2和n/2+1—n后(这里为了便于叙述,就不讨论n/2不是整数的情况了,在编写程序时可以用很简单的取整函数解决这个问题),如果只在这两个部分再次进行分解和计算的话,就会忽略解是贯穿两个部分的数组这样的可能性,所以必须要在分的时候计算经过n/2的最大子数组,求解这个子数组的方式就是分别从n/2和n/2+1往左和往右对其他元素进行相加和比较,直到找到两边的最大值,然后进行相加即得到。
#求解中间的最大子数组 def find_max_cross(l1,left,right): #初始化左右两边的和以及解的坐标 leftmax=-float('inf') rightmax=-float('inf') ileft=-float('inf') iright=-float('inf') maxlist=[] sum=0 mid=(left+right)//2 #以mid为右端坐标往左进行寻找和的最大值 for i in range(mid,left-1,-1): sum=sum+l1[i] if sum>leftmax: leftmax=sum ileft=i sum=0 #以mid+1为左端往右进行寻找和的最大值 for i in range(mid+1,right+1,1): sum=sum+l1[i] if sum>rightmax: rightmax=sum iright=i #将两侧的结果相加,并取得相应的坐标返回 maxlist=[leftmax+rightmax,ileft,iright] return maxlist 复制代码在找到中间的最大子数组之后,剩余的可能为解的子数组就只能在1–n/2和n/2+1–n这两个区间里了,而在这两个区间里的解就可以通过不断的迭代来求解,然后在每次迭代时比较三者的大小从而取其最大值。
#分治法进行迭代 def find_all(l1,left,right): maxlist2=[] #最小规模:只有一个数字的子数组 if left==right: maxlist2=[l1[left],left,right] return maxlist2 else: #左右中三部分的最大子数组信息 lml=[] rml=[] mml=[] m=(left+right)//2 #主函数find_all的迭代,迭代的同时也开始在“治” lml=find_all(l1,left,m) rml=find_all(l1,m+1,right) mml=find_max_cross(l1,left,right) #进行比较取最大值 if lml[0]>=rml[0] & lml[0]>=mml[0]: return lml elif rml[0]>=lml[0] & rml[0]>=mml[0]: return rml else: return mml 复制代码分治法最终通过函数自身的迭代将复杂度为n2的问题简化到nlgn,这其中思维的难点就是对函数迭代的理解,既要会“分”,也要能“治”,而编程的难点在于迭代过程中的一些参数取值的范围和边界的细节问题