“《编程珠玑》(第2版)第8章”:连续子向量的最大和(扫描算法)

  问题是这样子的:

  输入是具有n个浮点数的向量x,输出是输入向量的任何子向量中的最大和。

 

  本文部分参考自一博文

  对于这道题,作者给出了总共4种不同方法:

  1. 直接解法

  最直接的方式是遍历所有可能的连续子向量,用i和j分别表示向量的首元和最后的尾元,k表示真实的尾元:

 1 int maxsum1(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int maxsofar = 0;
 5     int sum = 0;
 6     for (int i = 0; i < len; i++)
 7     {
 8         for (int j = i; j < len; j++)
 9         {
10             sum = 0;
11             for (int k = i; k < j + 1; k++)
12             {
13                 sum += *(arr + k);
14             }
15             maxsofar = max(maxsofar, sum);
16         }
17     }
18     return maxsofar;
19 }

  2. O(n2)的解法

  第1种方法的代码具有显而易见的浪费:对于一个子序列可能重复计算了多次。并且具有O(n3)的时间复杂度。其实k是多余的,依靠首尾两个变量i、j足以表示一个子向量。同时,j增长时,可以直接使用上一次的计算和与新增元素相加。因此改写为:

 1 int maxsum2(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int maxsofar = 0;
 5     int sum = 0;
 6     for (int i = 0; i < len; i++)
 7     {
 8         sum = 0;
 9         for (int j = i; j < len; j++)
10         {
11             sum += *(arr + j);
12             maxsofar = max(maxsofar, sum);
13         }
14     }
15     return maxsofar;
16 }

  另外一方面,由这个避免重复计算累加和的角度出发,构造一个累加和数组cumarr,cumarr[i]表示array[0...i]各个数的累加和,这样,array[i...j]的和就可以用cumarr[j]-cumarr[i-1]来表示了。考虑到边界值,令cumarr[-1]=0。在C/C++中的做法是令cumarr指向一个数组的第1个元素,cumarr = recumarr +1。有了cumarr[]就可以遍历所有的i、j来求最大值了:

 1 int maxsum3(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int maxsofar = 0;
 5     int sum = 0;
 6     int * cumarr = new int[len + 1];
 7     cumarr[0] = 0;
 8     for (int i = 0; i < len; i++)
 9     {
10         cumarr[i + 1] = cumarr[i] + *(arr + i);
11     }
12 
13     for (int i = 0; i < len; i++)
14     {
15         for (int j = i; j < len; j++)
16         {
17             sum = cumarr[j + 1] - cumarr[i];
18             maxsofar = max(maxsofar, sum);
19         }
20     }
21 
22     delete cumarr;
23     return maxsofar;
24 }

  虽然这个累加和数组的解法与后面两个相比,时间复杂度远不是最优的,然而这种数据结构很有用。

  累积表这种想法值得注意!

  3. 分治法(nlogn)

  分治法的基本思想是,把n个元素的向量分成两个n/2的子向量,递归地解决问题再把答案合并。

  分容易,合并就要花点心思了。因为对于初始大小为n的向量,它的最大连续子向量可能整体在分成的两个子向量中之一,也可能跨越了两个子向量。每次合并都需要计算这个跨越分界点的最大连续子向量,占据了很大的开销。

 1 int maxsum4(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int maxsofar = 0;
 5     if (len == 1)
 6     {
 7         maxsofar = arr[0];
 8     }
 9     else
10     {
11         int sum = 0;
12         int l = 0;
13         int u = len - 1;
14         int m = (l + u) / 2;
15 
16         // find max crossing to left
17         int lmax = 0;
18         for (int i = m; i >= 0; i--)
19         {
20             sum += *(arr + i);
21             lmax = max(lmax, sum);
22         }
23 
24         // find max crossing to right
25         int rmax = 0;
26         sum = 0;
27         for (int i = m + 1; i <= u; i++)
28         {
29             sum += *(arr + i);
30             rmax = max(rmax, sum);
31         }
32 
33         maxsofar = max(lmax + rmax, maxsum4(arr, m + 1), maxsum4(arr + m + 1, u - m));
34     }
35 
36     return maxsofar;
37 }

  延伸:分治法的最坏情况讨论

  根据合并过程,如果每次的最长子向量都恰好位于边界,即下图中灰色部分,两者其中之一:

  

  结果导致每次合并时都重复计算了在左边和右边的最长子向量,相当的浪费。解决方法是返回值中给出边界,如果边界在分界点上,合并时就不需要重复计算了。

  4. 扫描算法

  从头到尾扫描数组,扫描至array[i]时,可能的最长子向量有两种情况:要么在前i-1个元素中,要么以i结尾。前者的大小记为maxsofar,后者记为maxendinghere。

 1 int maxsum5(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int maxsofar = 0;
 5     int maxendinghere = 0;
 6     for (int i = 0; i < len; i++)
 7     {
 8         maxendinghere = max(maxendinghere + *(arr + i), 0);
 9         maxsofar = max(maxsofar, maxendinghere);
10     }
11     return maxsofar;
12 }

  这个算法可以看做是动态规划,把长度为n的数组化成了递归的子结构,并从首开始扫描求解。时间复杂度只有O(n)。

  注:直到最近做到LeetCode的题目,才发现这个扫描算法并不适用于存在负数的数组。对于存在负数的数组,可用下述解法:

 1     int maxSubArray(vector<int>& nums) {
 2         int sz = nums.size();
 3         if(sz == 0)
 4             return 0;
 5             
 6         int maxsofar = INT_MIN;
 7         int sum = 0;
 8         for(int i = 0; i < sz; i++)
 9         {
10             sum += nums[i];
11             if(sum > maxsofar)
12                 maxsofar = sum;
13             if(sum < 0)
14                 sum = 0;
15         }
16         
17         return maxsofar;
18     }

  

  同样的,利用该算法,我们还可以求总共最小的连续子向量:

 1 int minsum(int * arr, int len)
 2 {
 3     assert(len > 0);
 4     int minsofar = 0;
 5     int minendinghere = 0;
 6     for (int i = 0; i < len; i++)
 7     {
 8         minendinghere = min(minendinghere + *(arr + i), 0);
 9         minsofar = min(minsofar, minendinghere);
10     }
11 
12     return minsofar;
13 }

  

  附整个程序:

  1 #include <iostream>
  2 #include <stdlib.h>
  3 #include <cassert>
  4 using namespace std;
  5 
  6 int max(int a, int b);
  7 int max(int a, int b, int c);
  8 int min(int a, int b);
  9 
 10 int maxsum1(int * arr, int len);
 11 int maxsum2(int * arr, int len);
 12 int maxsum3(int * arr, int len);
 13 int maxsum4(int * arr, int len);
 14 int maxsum5(int * arr, int len);
 15 
 16 int minsum(int * arr, int len);
 17 
 18 int main()
 19 {
 20     int arr[8] = {5, -2, -3, 4, -2, -8, 10, -5};
 21     int len = sizeof(arr) / sizeof(int);
 22 
 23     int maxsofar1 = maxsum1(arr, len);
 24     cout << "Maxsofar1 = " << maxsofar1 << endl;
 25 
 26     int maxsofar2 = maxsum2(arr, len);
 27     cout << "Maxsofar2 = " << maxsofar2 << endl;
 28 
 29     int maxsofar3 = maxsum3(arr, len);
 30     cout << "Maxsofar3 = " << maxsofar3 << endl;
 31 
 32     int maxsofar4 = maxsum4(arr, len);
 33     cout << "Maxsofar4 = " << maxsofar4 << endl;
 34 
 35     int maxsofar5 = maxsum5(arr, len);
 36     cout << "Maxsofar5 = " << maxsofar5 << endl;
 37 
 38     int minsofar = minsum(arr, len);
 39     cout << "Minsofar = " << minsofar << endl;
 40 
 41     return 0;
 42 }
 43 
 44 int max(int a, int b)
 45 {
 46     return (a > b) ? a : b;
 47 }
 48 
 49 int max(int a, int b, int c)
 50 {
 51     int temp = max(a, b);
 52     return (temp > c) ? temp : c;
 53 }
 54 
 55 int min(int a, int b)
 56 {
 57     return (a < b) ? a : b;
 58 }
 59 
 60 int maxsum1(int * arr, int len)
 61 {
 62     assert(len > 0);
 63     int maxsofar = 0;
 64     int sum = 0;
 65     for (int i = 0; i < len; i++)
 66     {
 67         for (int j = i; j < len; j++)
 68         {
 69             sum = 0;
 70             for (int k = i; k < j + 1; k++)
 71             {
 72                 sum += *(arr + k);
 73             }
 74             maxsofar = max(maxsofar, sum);
 75         }
 76     }
 77     return maxsofar;
 78 }
 79 
 80 int maxsum2(int * arr, int len)
 81 {
 82     assert(len > 0);
 83     int maxsofar = 0;
 84     int sum = 0;
 85     for (int i = 0; i < len; i++)
 86     {
 87         sum = 0;
 88         for (int j = i; j < len; j++)
 89         {
 90             sum += *(arr + j);
 91             maxsofar = max(maxsofar, sum);
 92         }
 93     }
 94     return maxsofar;
 95 }
 96 
 97 int maxsum3(int * arr, int len)
 98 {
 99     assert(len > 0);
100     int maxsofar = 0;
101     int sum = 0;
102     int * cumarr = new int[len + 1];
103     cumarr[0] = 0;
104     for (int i = 0; i < len; i++)
105     {
106         cumarr[i + 1] = cumarr[i] + *(arr + i);
107     }
108 
109     for (int i = 0; i < len; i++)
110     {
111         for (int j = i; j < len; j++)
112         {
113             sum = cumarr[j + 1] - cumarr[i];
114             maxsofar = max(maxsofar, sum);
115         }
116     }
117 
118     delete cumarr;
119     return maxsofar;
120 }
121 
122 int maxsum4(int * arr, int len)
123 {
124     assert(len > 0);
125     int maxsofar = 0;
126     if (len == 1)
127     {
128         maxsofar = arr[0];
129     }
130     else
131     {
132         int sum = 0;
133         int l = 0;
134         int u = len - 1;
135         int m = (l + u) / 2;
136 
137         // find max crossing to left
138         int lmax = 0;
139         for (int i = m; i >= 0; i--)
140         {
141             sum += *(arr + i);
142             lmax = max(lmax, sum);
143         }
144 
145         // find max crossing to right
146         int rmax = 0;
147         sum = 0;
148         for (int i = m + 1; i <= u; i++)
149         {
150             sum += *(arr + i);
151             rmax = max(rmax, sum);
152         }
153 
154         maxsofar = max(lmax + rmax, maxsum4(arr, m + 1), maxsum4(arr + m + 1, u - m));
155     }
156 
157     return maxsofar;
158 }
159 
160 int maxsum5(int * arr, int len)
161 {
162     assert(len > 0);
163     int maxsofar = 0;
164     int maxendinghere = 0;
165     for (int i = 0; i < len; i++)
166     {
167         maxendinghere = max(maxendinghere + *(arr + i), 0);
168         maxsofar = max(maxsofar, maxendinghere);
169     }
170     return maxsofar;
171 }
172 
173 int minsum(int * arr, int len)
174 {
175     assert(len > 0);
176     int minsofar = 0;
177     int minendinghere = 0;
178     for (int i = 0; i < len; i++)
179     {
180         minendinghere = min(minendinghere + *(arr + i), 0);
181         minsofar = min(minsofar, minendinghere);
182     }
183 
184     return minsofar;
185 }
View Code

 

 课后相关习题

  10. 假设我们想要查找的是总和最接近0的子向量,而不是具有最大总和的子向量。你能设计出的最有效的算法是什么?可以应用哪些算法设计技术?如果我们希望查找总和最接近某一给定实数t的子向量,结果又将怎样?

  法一:利用累积表。我们定义cumarr[i]=arr[0]+arr[0]+...+arr[i]。如果有arr[l]=arr[u](l != u),则l..u之间的元素(不包括第l、u元素)总和为0。这个就也能够拓展到查找总和接近某一给定实数t的子向量。

  法二:如果我们不考虑子向量一定要连续这个要求,我们可以先将原向量按升序进行排序,再求cumarr向量。如果cumarr[i]=0,则表示0..i(包括第0、i元素)为所求子向量。这个同样也能够拓展到查找总和接近某一给定实数t的子向量。

   注:此题若用扫描算法来解决,问题比较多。

 

  13. 在最大子数组问题中,给定n*n的实数数组,我们需要求出矩形子数组的最大总和。该问题的复杂度如何?

  个人觉得很简单的就是把二维的降为一维的。如果是要求连续子向量,按照扫描算法就可以做到;如果不要求连续,则先按降序进行排序,很快就能够求出最大总和的子向量。

 

posted @ 2015-04-01 22:14  峰子_仰望阳光  阅读(894)  评论(0编辑  收藏  举报