欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

算法设计与分析 - 分区策略(I):基础知识

最编程 2024-05-21 14:40:05
...


分类目录:​​《算法设计与分析》总目录​

许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:


  1. 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例。
  2. 解决这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解。
  3. 合并这些子问题的解成原问题的解。

分治算法分析

当一个算法包含对其自身的递归调用时,我们往往可以用递归方程或递归式来描述其运行时间,该方程根据在较小输入上的运行时间来描述在规模为 n n n的问题上的总运行时间。然后,我们可以使用数学工具来求解该递归式并给出算法性能的界。

分治算法运行时间的递归式来自基本模式的三个步骤。如前所述,我们假设 T ( n ) T(n) T(n)是规模为 n n n的一个问题的运行时间。若问题规模足够小,如对某个常量 c c c, n ≤ c n \leq c n≤c,则直接求解需要常量时间,我们将其写作 Θ ( 1 ) \Theta(1) Θ(1)。假设把原问题分解成 a a a个子问题,每个子问题的规模是原问题的 1 b \frac{1}{b} b1​。对于《​​排序算法(二):归并排序​​》中介绍的归并排序, a a a和 b b b都为2,然而,我们将看到在许多分治算法中 a ≠ b a \neq b a​=b。为了求解一个规模为 n b \frac{n}{b} bn​的子问题,需要 T ( n b ) T(\frac{n}{b}) T(bn​)的时间,所以需要 a T ( n b ) aT(\frac{n}{b}) aT(bn​)的时间来求解 a a a个子问题。如果分解问题成子问题需要时间 D ( n ) D(n) D(n),合并子问题的解成原问题的解需要时间 C ( n ) C(n) C(n)),那么得到递归式:

归并排序算法的分析

虽然MERGE-SORT的伪代码在元素的数量不是偶数时也能正确地工作,但是,如果假定原问题规模是 2 2 2的幂,那么基于递归式的分析将被简化。这时每个分解步骤将产生规模刚好为 n 2 \frac{n}{2} 2n的两个子序列。

下面我们分析建立归并排序 n n n个数的最坏情况运行时间 T ( n ) T(n) T(n)的递归式。归并排序一个元素需要常量时间。当有 n > 1 n > 1 n>1个元素时,我们分解运行时间如下:


  • 分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此,
  • 解决:我们递归地求解两个规模均为将贡献

  • 合并:我们已经注意到在一个具有n个元素的子数组上过程MERGE需要Θ(n)的时间,所以

当为了分析归并排序而把函数C(n)相加时,我们是在把一个函数与另一个函数相加。相加的和是 n n n的一个线性函数,即。把它与来自“解决”步骤的项相加,将给出归并排序的最坏情况运行时间的递归式:

T ( n ) = { Θ ( 1 ) , n = 1 2 T ( n 2 ) + Θ ( n ) , n > 1 T(n)=\left\{ \begin{aligned} \Theta(1) \quad \quad \quad \quad \quad , n = 1 \\ 2T(\frac{n}{2}) + \Theta(n) \quad , n > 1 \end{aligned} \right. T(n)=⎩⎨⎧​Θ(1),n=12T(2n​)+Θ(n),n>1​

在后文中,我们将看到“主定理”,可以用该定理来证明 T ( n ) = Θ ( n lg ⁡ n ) T(n) = \Theta(n\lg{n}) T(n)=Θ(nlgn),其中 lg ⁡ n \lg{n} lgn代表$log{n}$。因为对数函数比任何线性函数增长要慢,所以对足够大的输入,在最坏情况下,运行时间为 Θ ( n lg ⁡ n ) \Theta(n\lg{n}) Θ(nlgn)的归并排序将优于运行时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2)的插入排序。

为了直观地理解递归式的解为什么是 T ( n ) = Θ ( n lg ⁡ n ) T(n) = \Theta(n\lg{n}) T(n)=Θ(nlgn),我们并不需要主定理。把递归式重写为:

T ( n ) = { c , n = 1 2 T ( n 2 ) + c , n > 1 T(n)=\left\{ \begin{aligned} c \quad \quad \quad \quad \quad , n = 1 \\ 2T(\frac{n}{2}) +c \quad , n > 1 \end{aligned} \right. T(n)=⎩⎨⎧​c,n=12T(2n​)+c,n>1​

其中常量 c c c代表求解规模为 1 1 1的问题所需的时间以及在分解步骤与合并步骤处理每个数组元素所需的时间 e e e。

下图揭示了如何求解递归式。为方便起见,假设 n n n刚好是2的幂。图的(a)部分图示了 T ( n ) T(n) T(n),它在(b)部分被扩展成一棵描绘递归式的等价树。项 c n cn cn是树根(在递归的顶层引起的代价),根的两棵子树是两个较小的递归式 T ( n 2 ) T(\frac{n}{2}) T(2n​)。©部分图示了通过扩展 T ( n 2 ) T(\frac{n}{2}) T(2n​)再推进一步的过程。在第二层递归中,两个子结点中每个引起的代价都是 c n 2 \frac{cn}{2} 2cn​。我们通过将其分解成由递归式所确定的它的组成部分来继续扩展树中的每个结点,直到问题规模下降到1,每个子问题只要代价 c c c。(d)部分图示了结果递归树。

算法设计与分析——分治策略(一):基础知识_算法

接着,我们把穿过这棵树的每层的所有代价相加。顶层具有总代价 c n cn cn,下一层具有总代价 c ( n 2 ) + c ( n 2 ) = c n c(\frac{n}{2}) + c(\frac{n}{2}) = cn c(2n​)+c(2n​)=cn,下一层的下一层具有总代价 c ( n 4 ) + c ( n 4 ) + c ( n 4 ) + c ( n 4 ) = c n c(\frac{n}{4}) + c(\frac{n}{4}) + c(\frac{n}{4}) + c(\frac{n}{4}) = cn c(4n​)+c(4n​)+c(4n​)+c(4n​)=cn,等等。一般来说,顶层之下的第 i i i层具有 2 2 2个结点,每个结点贡献代价 c ( n 2 ) c(\frac{n}{2}) c(2n​),因此,顶层之下的第 i i i层具有总代价 2 c ( n 2 ) 2c(\frac{n}{2}) 2c(2n​)。底层具有 n n n个结点,每个结点贡献代价 c c c,该层的总代价为 c n cn cn。

上图中递归树的总层数为 lg ⁡ n + 1 \lg{n} + 1 lgn+1。其中 n n n是叶数,对应于输入规模。一种非形式化的归纳论证将证明该断言。 n = 1 n = 1 n=1时出现基本情况,这时树只有一层。因为 lg ⁡ 1 = 0 \lg{1} = 0 lg1=0,所以有 lg ⁡ n + 1 \lg{n} + 1 lgn+1给出了正确的层数。作为归纳假设,现在假设具有 2 i 2^i 2i个叶的递归树的层数为 lg ⁡ 2 i + 1 = i + 1 \lg{2^i} + 1 = i + 1 lg2i+1=i+1。因为我们假设输入规模是 2 2 2的幂,所以下一个要考虑的输入规模是 2 i + 1 2^{i+ 1} 2i+1。具有 n = 2 i + 1 n = 2^{i+ 1} n=2i+1个叶的一棵树比具有 2 2 2个叶的一棵树要多一层,所以其总层数为 lg ⁡ 2 i + 1 + 1 \lg{2^{i + 1}} + 1 lg2i+1+1

为了计算递归式表示的总代价,我们只要把各层的代价加起来。递归树具有 lg ⁡ n + 1 \lg{n} + 1 lgn+1层,每层的代价均为 c n cn cn,所以总代价为 c n ( lg ⁡ n + 1 ) = c n lg ⁡ n + c n cn(\lg{n} + 1) = cn\lg{n} + cn cn(lgn+1)=cnlgn+cn。忽略低阶项和常量 c c c便给出了期望的结果 Θ n lg ⁡ n \Theta{n\lg{n}} Θnlgn。