《编程珠玑》笔记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];