本节内容来源于Network Flows Theory, Algorithms, and Applications3.2节,主要介绍复杂度分析(complexity analysis)的相关知识。
1. 主流的复杂度分析方法(complexity measures)
1)实证分析法(empirical analysis):估计算法在实践中的表现。此方法的缺点有:其结果过度取决于编程语言、编译器和计算机,耗时耗力,且可能自相矛盾。
2)平均情况分析法(average-case analysis):估计算法所需步骤数目的期望。此方法的缺点有:其结果主要取决于所选择实例的概率分布,不同实例间差别较大;使用时的实例往往很难确定适当的概率分布;分析需要较为复杂的数学算法;即便平均性能良好,在解决部分问题时表现也未必好。
3)最坏情况分析法(worst-case analysis):给定算法在任意实例中采取步骤数目的上界。此方法解决了上述方法的大部分缺点,但也有不足——允许“病理”实例来确定算法性能,但这种极端情况实际可能并不多见。
目前,最坏情况分析法由于其分析算法性能的巨大优势,在科学研究中占绝大多数。
2. 问题的规模(problem size)
1)首先提出问题:如何衡量一个问题的规模?
2)为回答这一问题,首先从另一个简单的问题入手:一个值为x的数据单元的规模是多大?
两种表示:x或log x。显然第二种更加合理,因为计算机是以二进制的方式存储数据。存储x的空间与log x成正比。
3)一个网络流问题的规模即为该网络表示方法(见2.3节)的一个函数。以空间利用率最高的邻接表方法为例,此问题的规模即为存储此邻接表所需的位数。由于邻接表①为每个节点和弧存储一个指针②为每个弧容量和成本存储一个数据,在最小成本流问题中,共需存储:
其中,C代表最大弧成本,U代表最大弧容量。由于m≤n^2,log m≤2log n。我们会将运行时间表示为m,n,log C,log U的函数。
3. 最坏情况复杂度(worst-case complexity)
算法运行时间(running time)取决于输入的性质(nature)和规模(size)都有关。算法的时间复杂度函数(time complexity function)给出了一个算法运行所需要的最大时间。换句话说,它衡量了当问题规模增长时该算法运行时间的增长速度。时间复杂度函数也可以称作最坏情况复杂度(worst-case complexity)。
4. 大“O”表示法( big O notation)
先看一个例子:
乍一看我们会认为执行了pq次加法,并且将值存储到了cij中。实际上,计算机中是将p×q大小的二维数组存储为pq长度的数组,通常会将aij存储在(i-1)q+j的位置上。因此,每次检索aij或bij的值时,会执行一次减法、一次加法、一次乘法。每次运算后,还要将i与p作比较,j与q作比较。由此例可见,详细的计算分析是复杂的,没有太大意义。
因此,我们忽略时间复杂度中的常数,可以引入O。如果对于给定的c和n0,对于所有的n≥n0,任意实例算法所需的时间最多是cf(n),此算法的运行时间是O(f(n))。
此外,大“O”表示法只表示运行时间中最主要的项,其余的项随着n的增加都可忽略不计。我们忽略常数的另一个重要意义是假设所有的运算(加减乘除、比较、赋值、逻辑运算)都需要相等的时间。虽然这些操作的实际速度不同,但都会被一个有限常数所限制,因此在大“O”表示法中可以忽略。
5. 相似性假设(similarity assumption)
我们假设每个算术运算只需要一步,这在运算值巨大时可能会使我们低估运行时间。为避免这种对运行时间的系统性低估,在比较运行时间时,我们假设C(最大成本)和U(最大容量)都被限制在n的多项式里,将此假设称为相似性假设。
6. 多项式时间和指数时间算法(polynomial- and exponential- time algorithms)
1)如果最坏情况复杂度被问题参数的多项式函数所限制,则称此算法(多项式时间算法)为好的,例如。如果运行时间与log C、log U无关,则称为强多项式时间算法(strongly polynomial-time algorithm),否则称为弱多项式时间算法(weakly polynomial-time algorithm)。强多项式时间算法更好,因为它可以解决任意大的容量和成本问题。
2)讨论运行时间被网络参数或网络规模的多项式时间所限制是等价的,即网络参数n,m,log C,log U的多项式表示和nlog n+mlog m +nlog C+mlog U的多项式表示均可。
3)如果最坏情况复杂度不能被问题参数的多项式函数所限制,则称此算法为指数时间算法,例如。如果算法运行时间被参数n,m,C,U的多项式函数所限制,则称为伪多项式时间算法,是指数时间算法的重要子集,例如
。如果满足相似性假设(见本节5),伪多项式时间算法变为多项式时间算法,但当C、U是关于n的高次多项式时,此算法意义就不大了。
4)多项式时间算法在大多数情况下优于指数时间算法,但也有例外。比如单纯形法是一种指数时间算法,但它的应用要比多项式时间算法的Khachian椭球算法应用广泛。
7. 大“Ω”表示法和大“θ”表示法(big Ω and big θ notation)
1)大“Ω”表示法(big Ω notation):如果对于给定的c’和n0,对于所有的n≥n0,在某些实例上算法所需的时间至少是c’f(n),此算法的运行时间是Ω(f(n))。
要特别注意大“Ω”表示法和大“O”表示法定义上的区别。一个是“最多”和“至少”的区别,另一个是“任意实例”和“某些实例”的区别。
2)大“θ”表示法(big θ notation):如果一个算法既是O(f(n))又是Ω(f(n)),则称它为θ(f(n))。
如果我们证明了一个算法的上界和下界相同,那么这个算法不可能找到一个更“紧”的上界了。
8. 势函数和均摊复杂度(potential functions and amortized complexity)
引入:我们通常会计算每个基本操作所需的时间,用操作数×操作步数来限制运行时间的界。但有的时候,大多数步骤很容易执行,少数步骤花费时间较长,我们就可以使用均摊时间复杂度来描述它的时间复杂度。我们认为,用时长的操作所花的时间可以均摊到用时短的操作所花的时间上面。
我们以堆栈操作为例。执行将数据x压入堆栈S和弹出堆栈命令。入栈操作需要O(1)时间,出栈操作至多需要O(|S|)时间。如此计算n次操作(入栈或出栈)后,时间复杂度至多为O(|S|n)≤O(n^2)。但实际上,出栈的前提是栈非空,因此实际上的时间复杂度远没有预估的大。
势函数法是通过分析不同操作对适当函数的影响来建立时间复杂度的一种通用分析方法。令势函数。对于任意栈,我们都可以计算出它的势能函数对应的值。例如,有三个元素为1、3、5的堆栈S,其势能函数 Φ(S)=3。
容易得知:
- 空单调队列的势能函数为 0(即在算法开始时,势能函数为 0)
- 每次执行一个入队操作,堆栈的势能函数增加1。
- 执行弹出的时候,弹出多少个元素,势能函数就减少多少。
- 算法结束时,势能函数非负
所以我们就可以将出栈时的开销摊到入栈操作上。因为每次弹出一个元素,势能函数就会-1。而在入队操作的时候,势能函数+1。每次出栈和入栈的时间复杂度都是O(1),出队的时间复杂度显然也是O(1),则n次操作的总时间复杂度是O(n)。
9. 参数平衡(parameter balancing)
当运行时间是关于k的函数时,我们通常希望确定使运行时间最小的k值。例如,一种算法的时间复杂度是,我们希望找到一个最优的k值。
1)如果f和g关于k同时单调递增或单调递减,则k值可取可行域中的最小或最大值,问题解决。
2)如果f和g关于k一个递增,一个递减,可通过将f+g视作k的函数,采用微分法(differential calculus),求得k的最优值。
3)很多情况下,微分法无法获得最优解。此时可以采用参数平衡(parameter balancing)的方法。寻找一个k*使得f(n,m,k*)=g(n,m,k*)。那么,f(n,m,k*)+g(n,m,k*)至多是最小值的两倍。我们考虑以下最短路径算法的例子:。前一个函数关于k单调递减,后一个函数关于k单调递增。我们使
,得到
。由此,我们得到了最佳运行时间
。