求一个整数序列的最大子序列和(编程珠玑第八章)

前言这个问题貌似在今年4月份腾讯招聘实习生的第一轮笔试中曾出现过,不过不是一道编写程序题, 而是相对简单些的程序填空题:题目给出了问题描述后,要求分别按照o(n^2)和o(n)的算法复杂度来填空。 最近几天正忙着看一个孩子推荐的书籍《编程珠玑》,巧合的是在本书第八章便是围绕这个问题层层推进来展开论述的。

 

问题描述:给定一个整数序列 a1, a2, a3,  a4,a5......an, 求它的一个特殊子序列即:ai,a(i+1), a(i+2)......a(j) ;要求这个子序列的和在所有原序列的子序列中的和最大。补充说明:如果原序列全为负数,则所求特殊子序列为空,且规定,此时最大子序列的和为0。

问题分析:要求出子序列中和最大的那一个,初步的想法是:只要逐一比较所有子序列即可,即:求出所有的i,j∈[1,n] 并且逐一比较即可。由问题描述中空子串及其值的设定,可以在进行比较之前设置初始的最大子串和为0,程序(伪代码)如下:

input num_sequence[n + 1];
max = 0;
for (i = 1; i <= n ;i++) {
      for (j = i; j <= n; j++) {
           sum = 0;
           k = i;
           while (k <=j) {
                  sum += num_sequence[k];
                  k++; 
           }
                  if (sum > max)
                         max = sum;
      }
}

 一不小心,套了三次循环,算法复杂度o(n^3),如果这个序列长为1000000,估计有的你算的!《编程珠玑》给出了另外两个o(n^2)的方法:方法1、可以直接在把上述最外层的循环因子i看成是子序列起始位置的标示,第二层循环中的j仍标示子序列终止位置,然而与上述o(n^3)方法所不同的是这个方法不必根据i,j来重新循环计算i->j的子序列和来造成第三层循环,而是直接比较每一个固定的起始点i下,不同终点j的和得出局部最优,重而避免了重复计算前面的和,并且i循环到n后,自然就实现全局最优了,代码很显然,如下所示:

 

for (i = 1; i <= n ;i++) {
              sum = 0;
              for (j = i; j <= n; j++) {
                      sum += num_sequence[j];
                      if (sum > max)
                         max = sum;
             }
}

方法2、我们可以把它称作"累加逐差法", 首先这种方法求出原序列的逐项累加序列num_accumulate[1~n], 且num_accumulate[i] = num_sequence[1] + num_sequence[2]  ......+ num_sequence[i]; 根据这个序列,我们就能求出任意起始点为i,终止为j的子序列的和了:sum(i, j) = num_accumulate[j] - num_accumulate[i - 1];   这里为了保证统一性,设置num_accumulate[0] = 0;这样程序如下:

/*初始化num_accumulate数组*/
num_accumulate[0] = 0;
for (i = 1; i <= n; i++) {
      num_accumulate[i] = num_accumulate[i -1] + num_sequence[i];
}
max = 0;
sum = 0;
for (i = 1; i <= n; i++) {
      for (j = i; j <= n; j++) {
           sum = num_accumulate[j] - num_accumulate[i -1];
           if (sum > max)
                max = sum;
     }
}

相较于最初的方法而言,这两个方法的算法复杂度都下降了一个等级,但是,离最优的O(N)算法还是差了一个数量级~我们稍后进一步来分析这个问题,分别用分治法的思想得到一个o(nlogn)算法以及全面分析这个问题的性质的前提下得到一个o(n)最优算法。
 

1.用分治法得到的一个o(nlogn)算法

分治法是算法设计的一个常用方法,比如最熟悉的合并排序算法就是利用这一思想。本问题也可以参照这个思想来考虑。我们可以像合并排序那样,将整个整数原序列分成两个,重而“分而治之”:求出每一半序列中的最优子序列然后比较,但是不幸的事情还是发生了:最优子序列恰恰可能“横跨”两个“分治区域”摆在中间。。。。。。我们必须设法解决这儿问题。一种解决方法是从两个“半序列”黏合的头部分别向扫描各自半个序列得到从“粘合”处开始的两个具有最大和的序列,然后将这两个和相加得到”最优子序列和候选和1号“,再将它与两个“半序列”产生的两个“最优子序列和”中的较大者进行比较,得到整个序列的最优子序列和。根据上面分析次算法的复杂度:首先设整个复杂度用T(n)表示,那么半个序列复杂度即为T(n/2), 考虑到最优子序列可能存在于中间而进行的扫描复杂度应该为o(n),于是:T(n) = 2*T(n/2) + o(n) 计算这个递推式不难得到T(n) = o(nlogn)。整个算法的伪代码如下:

input sequence[1~n];
int max_sum(int start, int end)
{
    int mid = (start + end) /2; 
    int l_max, r_max,l_sum,r_sum, i; 
    if (start == end) 
        if (num_sequence[start]  0) 
			return 0; 
		else 
			return num_sequence[start]; 

    i = mid; 
	l_sum = l_max = 0; 
	while (i >= start) { 
		l_sum += num_sequence[i]; 
		if (l_sum > l_max) 
			l_max = l_sum; 
		i--; 
	} 

	i = mid + 1; 
	r_sum = r_max = 0; 
	while (i <= end) { 
		r_sum += num_sequence[i];
		if (r_sum > r_max)
			r_max = r_sum; 
		i++; 
	} 
    return max(max_sum(start, mid), max_sum(mid+1,end), l_max+r_max);
}


2.全面讨论此问题的性质,得到o(n)的最优算法

对于原序列a1,a2,a3......an的最优子序列ai, a(i+1)......aj;

将在以序列元素ai作为末端的子序列中,和最大的称作“ai为末端的最优子序列”记做"ai末最优";

将在以序列元素aj作为开始的子序列中,和最大的序列称作”aj为首端的最优子序列"记做"aj首最优";

有如下几条性质:
         性质1: 对于任意的k < i, 均有ak + a(k + 1)......a(i-1) <= 0;对于任意m  m > j && m <=n 有 a(j+1), a(j+2).....am <=0。 

         性质2: 设m ∈ 最优子区间[i,j], 那么 ai, a(i+1), a(i+2)......am 必然是"am末最优"; 同理, am, a(m+1)......a(j)必然是"am首最优"。

          性质3:任何一个最优子序列,即是它首端元素的ai的"ai首最优", 同时也是其末端元素aj的"aj末最优"。

记"aj末最优"的序列和为sum_e(j), "ai首最优"的序列和为sum_s(i)则有如下递推公式:

           sum_e(j) = aj + max(0, sum_e(j-1)); 

          sum_s(i) = ai + max(0, sum_s(i+1));

由于最优子序列既是某一序列元素ai的"末最优",也是某一序列元素的"首最优", 因此,可以用上述两个公式得出两个不同方向扫描的o(n)算法,求得最优子序列和, 以"末最优"为例,代码如下:

input num_sequence[1~n]

max = 0;   //最优子序列最大和
max_e = 0;//各个"末最优"最大和
//所谓最优序列和,可理解为所有"末最优"序列中的最大值
for (i = 1; i <= n; i++) {
      max_e = num_sequence[i] + max(0, max_e);
     if (max_e > max)
       max = max_e;
}


上述代码是不是看上去很easy,其实很多问题想通之后也就那么回事。嘿嘿~上面的一些性质是我自己杜撰的理论,不知道描述恰当与否,还请各位批评指正啊。

 算法思想总结:(书中总结了解决以上问题的几个思想)

1.保存状态,避免重复计算(貌似动态规划大都利用了这一思想)

2.计算前,可以预处理数据,保存在特定的数据结构中

3.可以采用分而治之的策略思想来解决问题

4.可以通过递归的思想将x(1~i)的解扩展成x(1~i+1)的解

5.累积表,类似于1

6.设计算法后估计算法复杂度下界

 

 

附言:这个问题实际上还可以继续扩展下面几个问题(可以利用解决上面问题的几个思想来解决)

1. 给定一个序列,求出一个子序列,其和最接近于0

2.给定一个子序列,求出一个序列,其和最小

3.给定一个子序列,其和的绝对值最大

4.给定一个子序列,其和最接近于一个给定的数r

5.给定一个子序列,其和的绝对值最接近一个给定的非负数r

猜你喜欢

转载自blog.csdn.net/tangchao52121/article/details/8450121