《编程珠玑》笔记8 算法设计技术

本章主要为了说明——一个良好的算法设计不仅能让程序更简单(第二章),更能提高程序的性能。

1.问题:连续子序列最大和问题

  具体描述:在一串可正可负的n个整数向量中,给出连续子序列的最大和。

  如给出{-2, 11, -4, 13, -5, -2}.

  输出应为 20,(也就是 11, -4, 13这三个子序列)

2.解法

  这个问题因为见过较多,四种解法也都看过,就当是复习吧:

   方法1:O(n^3),基本遍历方法,遍历每一个子数组,计算其和并比较。

 1 int method1(vector<int> data)
 2 {
 3     int len = data.size();
 4     int sum = INT_MIN;
 5     for(int i = 0; i < len; i++)
 6         for(int j = i; j < len; j++)
 7         {
 8             int temp = 0;
 9             for(int k = i; k <=j; k++)
10                 temp += data[k];
11             sum = max2(sum, temp);
12         }
13     return sum;
14 }

   方法2:在上面的最里层循环中,每次得到一个字数组的起点i和终点j后,k遍历计算这个子数组之和。但是我们知道,后一个子数组的和实际上就等于前一个子数组之和加上当前元素。所以可使用如下O(n^2)方法

 1 int method2(vector<int> data)
 2 {
 3     int len = data.size();
 4     int sum = INT_MIN;
 5     for(int i = 0; i < len; i++)
 6     {
 7         int temp = 0;
 8         for(int j = i; j < len; j++)
 9         {
10             temp += data[j];
11             sum = max2(sum, temp);
12         }
13     }
14     return sum;
15 }

  方法3:可以采用二分法来计算子数组的和,每次从中间分割数组,分别计算左边和右边最大子数组和(递归方法),在与可能跨过边界的子数组情况进行比较,选择三个数值中最大的即可。时间复杂度O(nlogn)

 1 int method3(const vector<int> & data, int l, int r)
 2 {
 3     //由于在递归时,左半部分递归使用了(l,mid)。而不是(l,mid-1),所以一定会出现l==r
 4     if(l == r)
 5     {
 6         if(data[l] < 0)
 7             return 0;
 8         else
 9             return data[l];
10     }
11 
12     int mid = (l+r)/2;
13 
14     int lsumborder, lsum;
15     lsumborder = lsum = 0;
16     for(int i = mid; i >=l; i--)    //左边界以mid为顶
17     {
18         lsum += data[i];
19         lsumborder = max2(lsumborder, lsum);
20     }
21 
22     int rsumborder, rsum;
23     rsumborder = rsum = 0;
24     for(int i = mid+1; i <=r; i++)    //右边界从mid+1开始
25     {
26         rsum += data[i];
27         rsumborder = max2(rsumborder, rsum);
28     }
29     int lmaxsum = method3(data, l, mid);
30     int rmaxsum = method3(data, mid+1, r);
31 
32     
33     return max3(lmaxsum, rmaxsum, lsumborder+rsumborder);
34 }

    方法4:线性时间,主要是注意到这样的事实:我们只需要知道最大子数组的和,不需要了解其位置。对于最大和的子数组,它的前缀和一定不是负值。也就是说,加入当前探测到从i开始到j结束的序列值为负,那么可以直接推进到j+1位置进行下一次试探。

  在下面的程序中,j始终表示序列的结束位置,就是从该位置结束的序列所拥有的最大子序列和。

 1 int method4(const vector<int> & data)
 2 {
 3     int maxSum = 0, thisSum = 0;
 4 
 5     for(int j = 0; j < data.size(); j++)
 6     {
 7         thisSum += data[j];
 8 
 9         if(thisSum > maxSum)
10             maxSum = thisSum;
11         else if(thisSum <0)
12             thisSum = 0;
13     }
14     return maxSum;
15 }

  最后,主函数如下:

 1 #include<iostream>
 2 #include<fstream>
 3 #include<vector>
 4 #include<climits>
 5 using namespace std;
 6 
 7 int max2(const int f, const int s)
 8 {
 9     return f > s? f: s;
10 }
11 int max3(const int f, const int s, const int t)
12 {
13     int temp = f > s? f: s;
14     return temp > t? temp : t;
15 }
16 
17 int main(int argc, char **argv)
18 {
19     ifstream fin(argv[1]);
20 
21     int t;
22     vector<int> data;
23     while(fin >> t)
24         data.push_back(t);
25 
26     cout << "method1: " << method1(data) << endl;
27     cout << "method2: " << method2(data) << endl;
28     cout << "method3: " << method3(data, 0, data.size()) << endl;
29     cout << "method4: " << method4(data) << endl;
30 
31     return 0;
32 }

  书上对于平方算法使用了两种方法,另一种方法是提前算出数组前j个元素的累加和,在确定了i和j后,只需curarr[j] - curarr[i-1]一步就可以确定子数组之和。

 1 curarr[-1] = 0
 2 for i = [0,n)
 3   curarr[i] = curarr[i-1] +x[i]
 4 
 5 curmax = 0;
 6 for i = [0,n)
 7   for j = [i,n)
 8     sum = curarr[j] - curarr[i-1]
 9     curmax = max2(sum, curmax);
10 
11 return curmax;

3.原理

  几个重要的算法设计技术:

  保存状态,避免重复计算:如method2利用前子数组和计算后一个子数组和; method4中thisSum变量的更新。

  将信息预处理至数据结构:算法2b中curarr结构(累加数组的运用,习题10、11、12)

  分治算法:method3

 4.习题

  4.5 如何实现访问curarr[-1]?

  可以进行如下转换:

  float realarray[length];

  float *curarr = realarray +1;

  4.9 最大子向量的和定义为最大元素的值,就是说最大和可能为负数。

  这对上面的方法没有太大的影响,只要令 记录当前最大值的curSum为INT_MIN或array[0]即可。

  对方法1和方法2,已经这样做了;

  对方法3,另lsum和rsum初始为INT_MIN, 递归结束条件l==r中去掉判断小于0.

    对方法4,另maxSum = INT_MIN

  4.10 如果要查找总和最接近0,或 最接近某个实数t的子向量。

  (由于问题的特点,方法3【lsumborder等不再适用】和方法4【很难确定如何推进,thisSum不好确定取舍】很难适用,所以采用暴力搜索的方法)

  最好的办法就是使用 累加数组,也即算法2b中使用的curarr。使用一个计数值 opt 来维护,当前子向量和距0的距离,如果有更小的值出现,就更新这个opt,对每一组(i,j)进行判断。

  4.11 驶过两个收费站,就是一段公路,汽车在行驶时,只能连续行驶,不会从这段公路跳到后面的公路。所以就符合连续子向量的求和问题。

  对于收费站i和j,curarr[j] - curarr[i-1]就表示在i和j内行驶的路段费用,并且只占用 curarr[n]的线性空间。

  4.13 求二维子数组的最大总和

  《编程之美》2.15节。

  采用暴力方法,需要遍历每个子数组,并计算和,O(N^4 * SUM).  (SUM为计算每个子数组的和的时间).

  动态规划的方法。

  将二维转化为一维。枚举矩形上下边界,对每一个确定的上下边界,只有左右边界是待定的,类似于一维子数组最大和的方式。

  4.14 这里确定了子数组的大小为m.

  一种是仍使用累加和;

  另一种是根据上一次计算出的sum[i-1]计算sum[i]. 其中 sum[i] = sum[i-1] - x[i-m-1] + x[i];

posted @ 2012-09-06 16:01  dandingyy  阅读(316)  评论(0编辑  收藏  举报