算法分析与优化策略
一、渐进时间复杂度
基本操作的数量往往可以写成关于“输入规模”的表达式,保留最大项并忽略系数后的简单表达式称为算法的渐进复杂度,用于衡量基本操作数随规模的增长情况。
例如:设输入规模为n时加法操作的次数为T(n),T(n) = n(n+1)(n+2)/6。
当n很大时,平方项和一次项对整个多项式的影响不大,可以用一个记号来表示:T(n) = θ(n3),或者说T(n)和n3同阶。
同阶指“增长情况相同”。
(渐进时间复杂度忽略了很多因素,因而分析结果只能作为参考,并不是精确的。尽管如此,如果抓住了最主要的运算量所在,算法分析的结果常常十分有用。)
二、上界分析
另一种推导时间渐进复杂度的方法:
下面是求最大连续和的代码
int f(int P[], int n) { int ans = P[0]; for(int i = 0; i < n; i ++) { for(int j = i; j < n; j ++) { int sum = 0; for(int k = i; k <= j; k ++) { sum += P[k]; if(sum > ans)ans = sum; } } } return ans; }
3重循环最坏情况下都需要n次,因此总运算次数不超过n3。上界的记号为T(n)=O(n3)。
在算法设计中,常常不进行精确分析,而是假定各种最坏的情况同时取到,得到上界。在很多情况下,这个上界和实际情况同阶(称为“紧”的上界),但也有可能会因为分析方法不够好,得到“松”的上界。
松的上界也是正确的上界,但可能让人过高的估计程序运行的实际时间(从而不敢编写程序),而即使上界是紧的,过大或过小的最高项系数同样可能引起错误的估计。换句话说,算法分析不是万能,要谨慎对待分析结果。如果预感到上界不紧、系数过大或者过小,最好还是编程实践。
ps.最大连续和的一种优化方法:
int f(int P[], int S[], int n) { S[0] = 0; int ans = P[1]; for(int i = 1; i <= n; i ++) { S[i] = S[i-1] + P[i];//S[i]代表从P[1]到P[i]的和 } for(int i = 1; i <= n; i ++) { for(int j = i; j <= n; j ++) { ans = max(ans, S[j] - S[i - 1]); } } return ans; } int main() { int n; cin >> n; int P[1000],S[1000]; for(int i = 1; i <= n; i ++) { cin >> P[i]; } cout << f(P, S, n) << endl; return 0; }
递推思想
用类似的方法可得出T(n)=O(n2)
三、分治
根据最大连续和算法的进一步优化,分析分治思想的性能。
分治算法一般分为以下3个步骤:
划分问题:把问题的实例划分成子问题。
递归求解:递归解决子问题。
合并问题:合并子问题的解得到原问题的解。
最大连续和的分治算法:
1、把序列分成元素个数尽量相等的两半;2、分别求出完全位于左半或者完全位于右半的最佳序列;3、求出起点位于左半、终点位于右半的最大连续和序列,并和子问题的最优解比较。
int f(int A[], int x, int y)//返回在[x,y)中的最大连续和 { if(x == y - 1) { return A[x]; } int mid = x + (y - x) / 2, ans = A[x]; ans = max(ans, f(A, x, mid)); ans = max(ans, f(A, mid, y)); int L = A[mid - 1],R = A[mid]; for(int i = x; i < mid; i ++) { int sum = 0; for(int j = i; j < mid; j ++) { sum += A[j]; } L = max(L, sum); } for(int i = mid; i < y; i ++) { int sum = 0; for(int j = i; j < y; j ++) { sum += A[j]; } R = max(R, sum); } ans = max(ans, L + R); return ans; }
是否可以像前面那样,得到tot(基本操作数)的数学表达式呢?注意求和技巧已经不再适应,需要用递归的思路进行分析:设序列长度为n的tot值为T(n),则T(n)=2T(n/2)+n,T(1)=1。其中2T(n/2)
是2次长度为n/2的递归调用,最后的n是合并的时间。
解方程得T(n)=θ(nlogn)。nlogn增长很慢,比如,当n扩大2倍时,运行时间的扩大倍数只是略大于2。
递归方程T(n)=2T(n/2)+θ(n),T(1)=1的解为T(n)=θ(nlogn)。可以用解答树证明这个理论,也可以作为一个重要结论记下来。
该方法的2个细节:1、左闭右开的“数组分割”。使得处理自然。
2、(x+y)/2和x+(y-x)/2。数学上等价,在计算机中,前者是朝零取整,后者是朝区间起点取整。
四、正确对待算法分析结果
假设机器速度是每秒108次基本运算,运算量为n3、n2、nlog2n、n、n!(排列)、2n(子集枚举)的算法,在1s之内能解决最大问题规模n,如表所示:
运算量 n! 2n n3 n2 nlog2n n
最大规模 11 26 464 10000 4.5*106 108
速度扩大两倍后 11 27 584 14142 8.6*106 2*108
n!和2n不仅解决的问题规模非常小,而且增长缓慢;nlog2n、n不仅解决问题的规模大,而且增长快。渐进时间复杂为多项式的算法称为多项式时间算法,也称有效算法。像n!和2n 这样低效的算法称为指数时间算法。
需要注意的是,分析结果在趋势上能反映算法的效率,但有2个不确定性:一是公式本身的不精确性(系数、非主流操作的影响);二是对程序实现细节与计算机硬件的依赖性。在不少情况下,算法实际能解决的问题和表所示有着较大差异。
但是还是有一定借鉴意义的,例如n<=8,n!的算法已经足够了,n<=20,需要用到2n的算法,n<=300,必须用至少n3的多项式时间算法。