括号生成(时间复杂度篇)

写在前面

  • 算法的实现(代码)以及算法的正确性(和所需的理论知识)已经在括号生成(理论及实现篇)写到,如果你没有看过请先大略浏览一遍,以防直接阅读本篇造成的无上下文的体验。
  • LeetCode官方题解对这两种算法的分析不仅有些简略而且不太严谨。

现在以上一篇为基础,对闭合数回溯法做算法复杂度分析。
C n = C 0 C n − 1 + C 1 C n − 2 + ⋯ + C n − 1 C 0 , n ≥ 1 (**) C_n=C_0C_{n-1}+C_1C_{n-2}+\cdots+C_{n-1}C_0,\quad n\ge 1\tag{**} Cn=C0Cn1+C1Cn2++Cn1C0,n1(**)

算法复杂度

  1. 闭合数(纯递归版)
//部分代码
for (int c = 0; c < n; ++c)
	for (String left: generateParenthesis(c))
		for (String right: generateParenthesis(n-1-c))
			ans.add("(" + left + ")" + right);
 

这一段循环实际上是翻译了一遍Catalan数的递推式 ( ∗ ∗ ) (**) (),我们自然可以得到
T g p R e c ( n ) = T g p R e c ( 0 ) T g p R e c ( n − 1 ) + T g p R e c ( 1 ) T g p R e c ( n − 2 ) + ⋅ ⋅ = ⋅ ⋅ + T g p R e c ( n − 1 ) T g p R e c ( 0 ) = ∑ k = 0 n − 1 T g p R e c ( k ) T g p R e c ( n − k − 1 ) \begin{aligned}T_{gpRec}(n) &=T_{gpRec}(0)T_{gpRec}(n-1)+T_{gpRec}(1)T_{gpRec}(n-2)+\cdot\cdot\\ &\phantom{=}\cdot\cdot+T_{gpRec}(n-1)T_{gpRec}(0) \\ &=\sum_{k=0}^{n-1}T_{gpRec}(k)T_{gpRec}(n-k-1)\end{aligned} TgpRec(n)=TgpRec(0)TgpRec(n1)+TgpRec(1)TgpRec(n2)+=+TgpRec(n1)TgpRec(0)=k=0n1TgpRec(k)TgpRec(nk1)

理论篇中说明了使用母函数法可以算出 T g p R e c ( n ) T_{gpRec}(n) TgpRec(n)的表达式(具体方法已经超出本文讨论范畴,不在此说明,可以自行查阅资料如《组合数学》),利用结论我们有
T g p R e c ( n ) = O ( 1 n + 1 ( 2 n n ) ) T_{gpRec}(n)=O(\frac 1 {n+1}\binom{2n}{n}) TgpRec(n)=O(n+11(n2n))

使用Striling公式 n ! ∼ 2 π n ( n e ) n n! \sim\sqrt{2\pi n}(\frac n e)^n n!2πn (en)n,对 ( 2 n n ) \binom{2n}{n} (n2n)作无穷量近似
( 2 n n ) = ( 2 n ) ! n ! n ! ∼ 4 π n ⋅ 4 n ⋅ ( n e ) 2 n 2 π n ⋅ ( n e ) 2 n = 4 n π n → 4 n n \begin{aligned}\binom{2n}{n} =\frac{(2n)!}{n!n!}&\sim\frac{\sqrt{4\pi n}\cdot4^n\cdot(\frac n e)^{2n}}{2\pi n\cdot(\frac n e)^{2n}}\\ &=\frac{4^n}{\sqrt{\pi n}}\rightarrow \frac{4^n}{\sqrt{ n}}\end{aligned} (n2n)=n!n!(2n)!2πn(en)2n4πn 4n(en)2n=πn 4nn 4n

所以可得
T g p R e c ( n ) = O ( 1 n + 1 ⋅ 4 n n ) = O ( 4 n n n ) T_{gpRec}(n)=O(\frac 1 {n+1}\cdot\frac{4^n}{\sqrt{ n}})=O(\frac{4^n}{n\sqrt{ n}}) TgpRec(n)=O(n+11n 4n)=O(nn 4n)

  1. 闭合数(动态规划版)
    递推式和1中的递归版完全一样
    T g p D P ( n ) = ∑ k = 0 n − 1 T g p D P ( k ) T g p D P ( n − k − 1 ) T_{gpDP}(n) =\sum_{k=0}^{n-1}T_{gpDP}(k)T_{gpDP}(n-k-1) TgpDP(n)=k=0n1TgpDP(k)TgpDP(nk1)

    由于我们对子问题的结果进行了保存处理,所以时间复杂度不仅由递推式决定还要考虑己保存的子问题结果,所以我们最终需要分析的是 g p D P gpDP gpDP算法对应的DAG(Directed Acyclic Graph, 有向无环图)中节点的个数。

    n = 3 n=3 n=3为例
    对于 g p R e c gpRec gpRec,函数调用关系图如下(部分省略)
    括号生成(时间复杂度篇)_第1张图片
    我们省去一些相同的调用,将上图变为DAG
    括号生成(时间复杂度篇)_第2张图片
    这也就是 g p D P gpDP gpDP所做的,节点个数为4,只需计算4次就能得到答案,于是
    T g p D P ( n ) = O ( n + 1 ) = O ( n ) T_{gpDP}(n)=O(n+1)=O(n) TgpDP(n)=O(n+1)=O(n)

  2. 回溯法
    这一算法用递推关系难以表示(确切的说这就是一个串行算法, 并不是任意一层递归都能进入添加)(这两个入口,大多时候只有一个入口),因为问题的规模不仅仅与 n n n 有关,还与当前开闭括号数有关,所以不能精确地表示 T ( n ) T(n) T(n)
    但如果简单的认为复杂度与结果的个数相近既不严谨也不正确,所以我们换一个角度,对函数调用次数进行分析(这样分析的理由使考虑到该算法过于“串行”)
    基于实际消耗的时间,让我们觉得更加需要重新分析

    • 回溯法
      在这里插入图片描述
    • 闭合数
      I . I. I. 纯递归
      在这里插入图片描述
      I I . II. II. 动态规划
      在这里插入图片描述

基于函数调用次数的复杂度分析(这种观点存在争议)

规定:

  • 适用于复杂度受递归层数影响较大的算法
  • 函数调用次数用 N ( n ) N(n) N(n)表示, n n n 表示输入的规模,意思是要得到参数为n时的结果,还需进行 N ( n ) N(n) N(n)次函数调用
  • 根据定义,我们不考虑第一次调用,即 N ( 0 ) = 0 N(0)=0 N(0)=0
  • 可以通过分析递归深度,与每层调用的大致情况进行定性分析
  1. 闭合数(纯递归)
    观察递推式 ( ∗ ∗ ) (**) ()
    C n = C 0 C n − 1 + C 1 C n − 2 + ⋯ + C n − 1 C 0 C_n=C_0C_{n-1}+C_1C_{n-2}+\cdots+C_{n-1}C_0 Cn=C0Cn1+C1Cn2++Cn1C0

    我们发现, C 0 , C 1 , ⋯   , C n − 1 C_0,C_1,\cdots,C_{n-1} C0,C1,,Cn1每个出现两次,反映在函数调用上,可表述为——参数为 n n n的调用,会进行两次参数为 i ( ∈ [ 0 , n − 1 ] ) i(\in[0,n-1]) i([0,n1])的调用,每个调用要得到相应结果还需进行 N ( i ) N(i) N(i)次调用,依次类推…
    于是可以写出递推式
    N ( n ) = 2 ( N ( n − 1 ) + ⋯ + N ( 1 ) + N ( 0 ) ) + 2 n N(n)=2(N(n-1)+\cdots+N(1)+N(0))+2n N(n)=2(N(n1)++N(1)+N(0))+2n

    简单的变形后可以计算出
    N ( n ) = 3 n − 1 = O ( 3 n ) N(n)=3^{n}-1=O(3^n) N(n)=3n1=O(3n)

  2. 回溯法
    可以先观察一些 n n n较小的 N ( n ) N(n) N(n)的值

i 1 2 3 4
N(i) 回溯法 2 7 21 63
闭合数 2 8 26 80

发现回溯法的 N ( n ) N(n) N(n)值的确比闭合数的小,且趋势逐渐增大

你可能感兴趣的:(算法)