和吴昊一起玩推理 Round 11(增刊) —— 最大子段和的五重境界

  

如 图,为庐山的一角,又诗曰——“横看成岭侧成峰,远近高低各不同”,风流苏轼写的诗,木有错,至少说明了一个道理,解决一个问题可以有不同的途径,而不同 的途径,风景是不一样的,可以理解为时间效率和空间效率不一样,也可以理解为思想不同(比如在排序中有许多平均时间复杂度为O(nlogn)的算法,但是 由于其实现思想不同而导致了风景不一样)

  Problem:

 给定长度为n的整数序列,a[1...n], 求[1,n]某个子区间[i , j]使得a[i]+…+a[j]和最大。或者求出最大的这个和.例如(-2,11,-4,13,-5,2)的最大子段和为20,所求子区间为[2,4](这里必须指明子区间必须是连续的,否则,毫无人性的暴力可能会暴成O(2^n))

 第一境:毫无人性的暴力——时间复杂度O(n^3)

 这里实际上是要穷举所有的[1,n]之间的区间,所以我们用两重循环,可以很轻易地做到遍历所有子区间,一个表示起始位置,一个表示终点位置(也就是start和end)。

 

 1 //起始点
 2  int start = 0;
 3  //结束点
 4  int end = 0;
 5  //定义最大值
 6  int max = 0;
 7  for(int i = 1; i <= n; ++i)
 8  {
 9    for(int j = i; j <= n;++j)
10    {
11      int sum = 0;
12      for(int k = i; k <=j; ++k)
13        sum += a[k];
14      if(sum > max)
15      {
16        start = i;
17        end = j;
18        max = sum;
19      }
20    }
21  }

 第二境:有人性的暴力——时间复杂度O(n^2)

 我在吴昊系列第一季,也就是Round 7的熄灯问题中说过,如果可以合理的利用数据直接的关系的话,可以将毫无人性的暴力降低为有人性的暴力。这里,我们采取另外一种策略来弥补人性的缺失,就 是利用前面的一些数据作为铺垫(这里并不要将以前的数据全部存储),来解决一些新的问题。比如这里,我们考虑到并不需要每次都重新从起始位置求和加到终点位置.可以充分利用之前的计算结果,所以两个下标依然是start和end,但是我们在将end不断变动的时候,将其前面叠加的结果做一定程度的保留,并累加到后面。

 

 1 int start = 0;//起始位置
 2  int end = 0;//结束位置
 3  int max = 0;
 4  for(int i = 1; i <= n; ++i)
 5  {
 6    int sum = 0;
 7    for(int j = i; j <= n;++j)
 8    {
 9      sum += a[j];
10      if(sum > max)
11      {
12        start = i;
13        end = j;
14        max = sum;
15      }
16    }
17  }

 第三境:分而治之——时间复杂度O(nlogn)

 《三国演义》中有这么一段,诸葛亮离开荆州之前问关羽,如果孙权打过来怎么办啊?攻击啊!如果曹操打过来怎么办啊?抵御啊!如果孙权和曹操一起打过来,怎么办呢?分治,木有错,分而治之,谓之分治。

 在排序中,有一种方法叫归并排序,也就是分而治之的这种思想的体现,这种策略将问题又更进了一步。

 考虑到所有子区间[start, end]只可能有以下三种可能性:

 在[1, n/2]这个区域内

 在[n/2+1, n]这个区域内

 起点位于[1,n/2],终点位于[n/2+1,n]内

 以上三种情形的最大者,即为所求. 前两种情形符合子问题递归特性,所以递归可以求出. 对于第三种情形,则需要单独处理. 第三种情形必然包括了n/2和n/2+1两个位置,这样就可以利用第二种穷举的思路求出:

 以n/2为终点,往左移动扩张,求出和最大的一个left_max

 以n/2+1为起点,往右移动扩张,求出和最大的一个right_max

  left_max+right_max是第三种情况可能的最大值

  

 1 int maxInterval(int *a, int left, int right)
 2  {
 3     //这里表示已经归并到尽头
 4     if(right==left)
 5       return a[left]>0 ? a[left]:0;
 6     int center = (left+right)/2;
 7     
 8     //左边区间的最大子段和
 9     int leftMaxInterval = maxInterval(a,left,center);
10     
11     //右边区间的最大子段和
12     int rightMaxInterval= maxInterval(a,center+1,right);
13  
14     //以下求端点分别位于不同部分的最大子段和
15  
16     //center开始向左移动
17     int sum = 0;
18     int left_max = 0;
19     //从中间往左边逐渐扫描
20     for(int i = center; i >= left; i--)
21     {
22        sum += a[i];
23        if(sum > left_max)
24           left_max = sum;
25     }
26     //center+1开始向右移动
27     sum = 0;
28     int right_max = 0;
29     //从中间往右边逐渐扫描
30     for(int i = center+1; i <= right; ++i)
31     {
32        sum += a[i];
33        if(sum > right_max)
34          right_max = sum;
35     }
36     //算出包含中间的情况的最大值,将这里的ret分别与左边和右边的最大值进行比较
37     int ret = left_max+right_max;
38     if(ret < leftMaxInterval)
39         ret = leftMaxInterval;
40     if(ret < rightMaxInterval)
41         ret = rightMaxInterval;
42     return ret;
43  }

 第四境:动态规划——时间复杂度O(n)

 DP的步骤:

 令b[j]表示以位置 j 为终点的所有子区间中和最大的一个

 

 如果b[j-1] >0, 那么显然b[j] = b[j-1] + a[j],用之前最大的一个加上a[j]即可,因为a[j]必须包含

 如果b[j-1]<=0,那么b[j] = a[j] (这里为赋予),因为既然最大,前面的负数必然不能使你更大

  DP的证明(来自算法导论中的剪切法):

 令a[x,y]表示a[x]+…+a[y] , y>=x

 假设以j为终点的最大子区间 [s, j] 包含了j-1这个位置,以j-1为终点的最大子区间[ r, j-1]并不包含其中

 即假设[r,j-1]不是[s,j]的子区间

 存在s使得a[s, j-1]+a[j]为以j为终点的最大子段和,这里的 r != s

 由于[r, j -1]是最优解, 所以a[s,j-1]<a[r, j-1],所以a[s,j-1]+a[j]<a[r, j-1]+a[j]

 与[s,j]为最优解矛盾

  DP的实现:

 

 1  int max = 0;
 2  int b[n+1];
 3  int start = 0;
 4  int end = 0;
 5  //初始化数组b[]
 6  memset(b,0,n+1);
 7  for(int i = 1; i <= n; ++i)
 8  {
 9    if(b[i-1]>0)
10    {
11      b[i] = b[i-1]+a[i];
12    }
13    else
14    {
15      b[i] = a[i];
16    }
17    if(b[i]>max)
18      max = b[i];
19  }
20  

 第五境——空之境界!(转载)

 任何一个游戏都可以无限地扩展,任何一个问题也如是,以下是一些基本的变式:

  变形1:最大子矩阵和
例子:HOJ 2558 maxsum

   所 谓万变不离其宗,这个的思路和上面是一样的. 维数增加了一维,所以可以考虑把它转化成一维的"基本问题". 我们可以先统计sum[i][j](以下假设下标从1开始) : 第i行,从开头到第j个元素的总值,这样, 第i行从第j个元素到第k个元素的总价值就是sum[i][k] - sum[i][j - 1]. 这个预处理的时间复杂度是O(N^2). 这时,这个问题就转化成了一维的最大子段和问题了: 枚举每一行中,第i到第j个元素(1 <= i <= j <= n),就可以把j - i + 1个元素的总和看成一个元素(转化的过程), 然后,对n个这样的元素求最大子段和即可. 这一部分的时间复杂度是O(N^3),因此总复杂度也为O(N^3).

 变形2:最大子立方体
例子:HOJ 2555 Eating Watermelon

   有 了上面的转化思路,这个问题也就很简单了,我们仍然应该进行转化. 这时,可以把平面上的一个子矩形看成一个元素,问题也轻松的转化成为一维的模型. 不过值得一提的是这时的预处理,可以设rec[x,y,z]表示z轴坐标为z的水平面中矩形(1,1,x,y)的数和。则z轴坐标为z的水平面中左上角为 (x1,y1)、右下角为(x2,y2)的矩阵的数和为rec[x2,y2,z] + rec[x1,y1,z] - rec[x2,y1,z] - rec[x1,y2,z](转自zhouguyue最大子段和问题的报告)  这个方法我是看了报告才知道的. 这个预处理过程是O(N^3),DP时的复杂度是O(N^5) (枚举一个平面上所有的子矩形,O(N^4); 求最大子段和,O(N); 相乘知总复杂度为O(N^5)),总复杂度也是O(N^5).

PS: 这个题我的代码跑了1.4X秒,差不多是最快的二倍,不知道这里有没有什么较好的优化方法!

 变形3:最大m子段和
例子: HDU 1024 Max Sum Plus Plus

这个变形和前面的稍有不同. 前面的背包问题中曾提到,"在限制条件增加一维时,可以将状态也相应的增加一维,来进行状态转移". 我觉得这个问题正是利用了这种强大思想! 可以增加一维状态,以dp[i][j]表示以第i个元素为结尾,使用j个子段所能达到的最大值 (这一维的状态,正是对应了新的限制条件!) 这样就很容易写出状态转移方程: dp[i][j] = max{ dp[i - 1][j] + a[i] (把第i个元素包含在最后一个子段内),    dp[i - k][j - 1] + a[i], j - 1 <= k < n - m + j(第i个元素单独为一子串).

   
不过,这里还有一个优化,是今天才学会的:为了避免重复计算(这也是DP的动机),可以令p[i][j]表示前i个元素的最大j子段和,这样p[i][j]
= max{p[i - 1][j], dp[i][j]}, dp[i][j] = max{f[i - 1][j], p[i - 1][j - 1]} + a[i]. 这个优化没有实践过,不知道效果如何!


 

posted on 2013-02-28 12:40  吴昊系列  阅读(184)  评论(0编辑  收藏  举报

导航