homework-01 最大子串和
题目描述
对于一个给定的数列,求该数列最大的子串和(连续)
问题分析
处理发生区间上的问题时,经常会用一个非常简单经典的思路——部分和(也有叫前缀和)。部分和的思想在很多复杂的区间上的算法中都有应用,它解决的问题是,在一个序列a[1..n]中快速求得任意子串a[n] + a[n + 1] + ... a[n + m]的和,具体过程非常简单,在原序列里,a[i]保存了在序列中第i个位置的数值,根据原序列生成一个新数列s[1..n],其中s[j] = sum{a[i], 0 <= i <= j},那么对于任意子串和就有a[p..q]=s[q]-s[p-1]。这样以来我们就可以在相同的空间复杂度下快速访问更多信息,这样完全不用保留原数组,因为对于任意原数组中的数值都有a[i] = s[i]-s[i-1]。有了新工具我们再来考虑原问题,先从最简单的思路,问题要求找出最大子串和,那么就枚举每一个子串的起始位置,找一个最大的子串,公式表示就是ans=max{s[j]-s[i],0<=i<=j,0<j<=n},从这个式子可以发现,任意子串和都有sum=s[j]-s[i],(0<=i<=j)的形式,其中s[j]的数值固定不同,那么我们只需要找到min{s[i],0<=i<j}就能找到以j结尾最大的子串,这个工作只要的经过简单的预处理就可以得到,那么我们最后实际需要做的工作就是枚举一遍子串的结尾点,结算该结尾最大的子串,并统计其中最大值即可,时空复杂度均为O(n)。
除了以上解法,我还在其他同学的博客发现了一些其他更好的解法,事实上我们发现一个序列里的数有正有负,其中最大子串是其中一些连续的数,这些数也是有正有负的,但其中真正“做出贡献”的只有正数,我们选取了一段负数是因为在这段负数的前后必定有贡献比其损失更多的正数,所以可以简单地得到一个结论,最大子串首尾必须是正数,(如果有一段时负数的话那么去掉这一段得到的子串比原来更大,与假设矛盾)。根据这个结论我们可以得到一个新的算法,把原数列分成正数段和负数段
原序列:
a=[1 -1 -2 2 3 -1 -1 3 4 5]
划分后:
(1) (-1 -2) (2 3) (-1 -1) (3 4 5) b=[1 -3 5 -2 12]
在新数列上我们枚举每一正数作为起点,然后不断加上他的后继,若当前结果优于已得到的答案则更新答案,当加入一个后继发现当前结果小于等于0时,就结束这次操作找下一个正数迭代。先在简单证明一下它的合理性,前面已经说明了最大子串肯定开始与正数,那么仅需证在每次枚举起始点的操作中都考虑到了所有情况,假设当前起始点为i,结尾枚举到了j时发现b[i..j]<=0,那么对于任意以j+1起始的子串来说,b[i..j] + b[j..k] < b[j..k],因此吧b[i..k]不会比b[j..k]更优,无需进行后面的枚举,证毕。但是现在看来这个算法最坏的时间复杂度是O(n^2),比前者逊色不少,但是这个算法还能再优化,如果以第i个位置为起点的查找到j点结束了,且存在一个b[k]>0,i<k<j,那么对于任意b[k..m],b[i..m]都会比b[k..m]优,因为在枚举过程中保证了b[i..k]>0,同时也保证了b[k..j]<0,那么当我们枚举到j结束之后以j+1处的元素为起始点继续枚举即可,如果全是非正数的话输出最大元素即可,这样这个算法就达到了O(n)的时间复杂度,达到了和前面算法一样的效率。
一些讨论
第二个算法想写上转载地址,但是实在懒得找了,原作者介意的话请速与我联系。在原博中作者自语此法为奇技淫巧(大概同意,忘了原话了),与标准答案略有差距,我个人还是非常喜欢这种接法的, 通过发现问题的新的性质来解决问题本身就会加深对问题的理解,同时带来新的启发,而且这种方法同表程相比代码长度,时空复杂度,思维难度大体相当,没有孰优孰劣之分。有了这些方法原题目现在看上去就so easy了,我们可以简单思考一下扩展问题。
求最小子串和;只需简单给每个数转换正负。
序列尾部会增加或删除元素;在第一种方案中给每个点记录一下当前状态,增删后从断点继续计算即可。
序列长度固定,其中一些元素的值会改变;可以使用标程中O(nlogn)的算法,将每次计算的节点信息保留,元素改变后同时改变影响的节点,时间复杂度为O((m+n)logn),其中m为改变的次数。
序列内部的元素会删除增加,没有想到什么更好的办法。
最大子串积,。。。。。
模m下的最大子串积,。。。。。
其他
我选择的参考书是第二版的《代码大全》