【经典问题】最大子串和
最近几天好好的研究了一下这个问题。
问题本身就不多说了,求一串数字中的所有子串中,和最大的一个子串。例如:
输入:-10 5 2 3 4 -5 -23 3 7 -21
输出:14 5 4
一、各种方法
方法1:
maxsofar = 0
for i = [0,n)
for j = [i,n)
sum = 0
for k =[i,j]
sum += x[k]
maxsofar = max(maxsofar,sum)
这是最直接最暴力的方法,我没有写,时间复杂度为O(n3),明显这里面有很多重复的运算,我们可以很容易的把时间复杂度降为O(n2)。实际上,我第一印象就是下面这个
方法2:
int s,e;
int maxsum = -999999;
int tempsum = 0;
for(int i=0;i<K;i++)
{
tempsum = 0;
for(int j=i;j<K;j++)
{
tempsum+=input[j];
if(maxsum < tempsum)
{
maxsum = tempsum;
s = i;
e = j;
}
}
}
这个方法理解为,以x[i]开头的所有子串中,和最大的一个。比枚举所有的 i 和 j 减少了计算量。下面一个比较重要的方法,虽然在时间复杂度上面没有提高,却包含了一个应对区间问题很重要的技巧。
方法3:
cumarr[-1] = 0
for i = [0,n)
cumarr[i] = cumarr[i-1] + x[i]
maxsofar = 0
for i = [0,n)
for j = [i,n)
sum = cumarr[j] - cumarr[i-1]
maxsofar = max(maxsofar,sum)
这个算法中,比较重要的一点事注意到子串和x[i...j] = cumarr[j] = cumarr[i-1],这个经验可以用在区间问题上。一个简单的列子:
n个收费站之间有n-1段路,每段路花费为P,用O(1)时间求任意两个收费站的之间的花费,要求空间为O(n)。
万能的二分能不能用到这个问题上,显然是可以的。在这个二分的过程中,需要注意的就是,合并结果的时候,需要注意到,跨左边跟右边的子串和的计算,最后就是在左边的最大子串、右边的最大子串、中间跨界的最大串这三者中取最大值。
方法4:
1 int max_sub(int m,int n)
2 {
3 if(m > n)
4 return 0;
5 if(m == n)
6 return max(0,input[m]);
7
8 int k = (m+n)/2;
9 //中间部分的结果显然是由以x[k]结尾的最大子串和
10 //和以x[k+1]开头的最大子串和 相加而得
11 int lmax,sum,a;
12 lmax = sum = a = 0;
13 for(int i=k;i>=m;i--)
14 {
15 sum += input[i];
16 if(lmax < sum)
17 {
18 lmax = sum;
19 a = i;
20 }
21 }
22 int rmax,b;
23 rmax = sum = b = 0;
24 for(int i=k+1;i<=n;i++)
25 {
26 sum += input[i];
27 if(rmax < sum)
28 {
29 rmax = sum;
30 b = i;
31 }
32 }
33 int max_l = max_sub(m,k);
34 int max_r = max_sub(k+1,n);
35 int result = max(lmax+rmax,max(max_l,max_r));
36 /*
37 how to record the start and the end
38 */
39 return result;
40 }
对于这个二分,还有一个待解决的问题,我想尝试一下,记录最大子串的起始位置和结束位置。个人对这种递归的理解确实不够,还没能够实现记录起始和结束位置。看来我还是得抽空好好把递归这个玩意儿好好理解一下。二分的话,显然时间复杂度为O(n*logn)。
接下来,就是O(n)的方法了,再来回顾一下方法3中,sum = cumarr[j] - cumarr[i-1]。要使得sum的值最大,显然cumarr[j]越大,cumarr[i-1]的值越小,sum的值越大。于是我们可以遍历cumarr数组,维持一个最小的cumarr[min_s],然后取cumarr[j]-cumarr[min_s]的最大值。
方法5:
1 min_s = -1//这里要初始化为-1
2 cumarr[-1] = 0;
3 for(int i=0;i<K;i++)
4 cumarr[i] = cumarr[i-1] + input[i];
5 for(int i=0;i<K;i++)
6 {
7 sum = cumarr[i] - cumarr[min_s];
8 if(maxsofar < sum)
9 {
10 maxsofar = sum;
11 s = min_s+1;
12 e = i;
13 }
14 if(cumarr[min_s] > cumarr[i])
15 min_s = i;
16 }
这里还有一个方法6:
1 int start = 0;
2 for(int i=0;i<K;i++)
3 {
4 if(maxendinghere+input[i]>0)
5 {
6 maxendinghere += input[i];
7 }
8 else if(maxendinghere + input[i] <= 0)
9 {
10 maxendinghere = 0;
11 start = i + 1;
12 }
13 if(maxendinghere > maxsofar)
14 {
15 maxsofar = maxendinghere;
16 e = i;
17 s = start;
18 }
19 }
对于这个方法我要转载一个比较好的解释:
-10 1 2 3 4 -5 -23 3 7 -21 (num)
-10 | 1 3 6 10 8 | -23 | 3 10 | -21 (Sum)(|号为该处Sum被清零)
由于10是Sum的最大值,所以,红色的那块就是要求的范围了。
这样就比较好理解第六种方法。
二、思考问题:
1.证明最大子串和的时间复杂度下届是O(n)
各位如果有思路或者资料麻烦告诉我一声。。。无处下手啊
2.求子串和最接近0的子串。
嗯,对于这个问题,前面的经验有 sum = cumarr[j]-cumarr[i-1],这样的话问题就转换成,求cumarr数组里面差值最小或者相等的两个元素。
用排序的方法,再遍历一次数组就可以得出结果,时间复杂度为O(n*logn)。 有没有更好的方法?
3.求子串和最接近t的子串。
这个问题,如果继续采用问题二的方案,问题转换成,在一个排好序的数组里面找两个值的差值为t,显然不能达到同样的效果。暂时没有想到其他更好的方法。
4.m和n为整数,给定x[n],求整数i,使得 x[i] + ... + x[i+m] 的和最接近为0。(即在第二个问题的基础上,加了条件: m个元素)
这个问题因为相差为m,扫一遍cumarr数组貌似就ok了。
min = max_num cumarr[-1] = 0 for i = [0,n) if(i+m<n) temp = cumarr[i+m]-cumarr[i-1] if (min > temp) min = temp start = i end = i+m
想要透彻的理解一个算法真的很难,我感觉我还是不太会思考。上面大部分内容是参考编程珠玑上面的内容,看懂正文好像不是太难,但是后面的习题就各种傻了。也许是我看得太少的缘故吧。
接下来想要看得主题 column 4 writing correct programs
参考资料:
《programming Pearls》
http://blog.csdn.net/hcbbt/article/details/10454947