非常复杂的递归代码的时间复杂度
Posted
技术标签:
【中文标题】非常复杂的递归代码的时间复杂度【英文标题】:Time complexity for a very complicated recursion code 【发布时间】:2015-12-14 15:05:55 【问题描述】:我在尝试计算这段代码的时间复杂度时遇到了一些问题:
function foo (int a):
if a < 1:
return 1
else:
for i = 1 to 4:
foo(a - 3)
for i = 1 to 4:
foo(a / 2)
end function
尽我所能:
T(n) = 1 if n<1
T(n) = 4T(n-3) + 4T(n/2) if n>=1
= 4(4T(n-6) + 4T((n-3)/2)) + 4(4T(n/2 - 3) + 4T(n/4))
~ 4^2 (T(n-6) + T((n-3)/2) + T(n/2-3) + T(n/4))
现在,这很复杂,因为下一个 T 的数量增加了 2^n 并且孩子也很复杂。
还有其他方法可以解决这个问题吗?
【问题讨论】:
Foo 最多只会返回 1 并发布代码 有经验的方法。使用 n=10, 100, 1000, 10.000, ... 运行它并绘制曲线。如果足够的话,它至少应该给你一个直觉 这在我看来不像 C 或 C++ 代码。是伪代码吗?第二个 for 循环的缩进是什么意思,它是否是“else”块的一部分?在 C / C++ 中不会这样。 @vmg:这不是真的,低端数据点将受到缓存、流水线、优化、内联以及芯片和编译器实际执行的各种其他事情的高度影响。如果你想进入算法的大 O 复杂度肯定占主导地位的状态,你必须运行非常大的数字。我认为尝试以这种方式“推断”大 O 复杂性通常不太实际。 @ChrisBeck:时间确实没有问题,但是添加一个对foo
的调用次数的计数器是微不足道的。
【参考方案1】:
让我们展开递归代价函数:
T(n) = 4 [T(n-3) + T(n/2)]
T(n) = 4^2 [T(n-6) + T((n-3)/2) + T((n-6)/2) + T(n/4)]
T(n) = 4^n [T(n-9) + 2*T((n-6)/2) + T((n-9)/2) + T((n-12)/4) + T((n-3)/4) + T((n-6)/4) + T(n/8)]
从T(x)
中的x
低于1
的那一刻起,您应该将T(x)
替换为1
。从那一刻起,T(x)
就不再产生任何“孩子”了。
这是什么意思?这意味着在k
-'th 扩展T(n)
之后,函数将如下所示:
T(n) = 4^k [number of paths with length `k`]
并不断增加k
直到所有路径都“死亡”。在n/3
迭代之后肯定是这种情况,因为这是可能的最长路径。
因此我们有某种图表,例如n=9
:
9 + 6 + 3 + 0
| | ` 1
| `3 + 0
| ` 1
`4 + 1
` 2 + -1
` 1
所以6
路径。现在的问题是如何计算路径的数量。为此,我们首先将主路径:n、n-3、n-6 等表示为一条水平线节点,这绝对是最长的路径:
n n-3 n-6 n-9 ... 1
现在在所有这些节点中,确实起源于 i -> i/2 的节点(除了一个)
n n-3 n-6 n-9 ... 4 1
| | | |
n/2 (n-3)/2 (n-6)/2 (n-9)/2 ... 2
(第二行显示了除以 2 创建的所有节点)。现在这些节点,再次生成后代 n -> n-3,也就是说,因为它被两个 n/2 -> (n-6)/2 整除,换句话说,有一些边会跳转两个:
n n-3 n-6 n-9 ... 4 1
| | /-----+-------(n-9)/2 |
n/2 (n-3)/2 (n-6)/2 (n-9)/2 ... 2
\---------->(n-6)/2 \------->...
换句话说,除了前两个元素,第二行中的所有其他节点都计为两个。如果我们将其表示为某种图,其中节点按权重标记,它看起来像:
1 -- 1 -- 1 -- 1 -- 1 -- .. -- .. -- 1
| | | | | | |
1 -- 1 -- 2 -- 2 -- 2 -- .. -- 2
或者如果我们在这个过程中继续这样做:
1 -- 1 -- 1 -- 1 -- 1 -- .. -- .. -- .. -- .. -- ..-- 1
| | | | | | | | | |
1 -- 1 -- 2 -- 2 -- 2 -- .. -- .. -- .. -- .. -- 2
| | | | | | | |
1 -- 1 -- 2 -- 2 -- 3 -- .. -- .. -- 4
(第三行进一步生成 4 个子项)
现在我们需要计算最后一行的总和。这最多是O(log n)
。
因此导致上限为O(4^(n/3)*log n)
最大值。绝对有可能边界更紧密,或者 4^(n/3+epsilon),log
在归结为指数时并不重要。
实验
可以将程序变成计算成本的程序(使用 Python):
def memodict(f):
""" Memoization decorator for a function taking a single argument """
class memodict(dict):
def __missing__(self, key):
ret = self[key] = f(key)
return ret
return memodict().__getitem__
@memodict
def foo (a):
if a < 1:
return 1
else:
return 1+4*(foo(a-3)+foo(a//2))
for i in range(1000) :
print '0 1'.format(i,foo(i))
注意1+
(这是因为调用不在叶子节点的方法也需要计算成本)。
它显示了下图(y 轴在日志空间中):
如果仔细观察,似乎log n
是一个更好的估计。虽然我不知道这样说是否安全。
这会产生一个表格(如下,进一步计算到2'000
)。
1 9
2 41
3 41
4 201
5 329
6 329
7 969
8 2121
9 2121
10 5193
11 9801
12 9801
13 22089
14 43081
15 43081
16 96841
17 180809
18 180809
19 395849
20 744009
21 744009
22 1622601
23 3015241
24 3015241
25 6529609
26 12149321
27 12149321
28 26290761
29 48769609
30 48769609
31 105335369
32 195465801
33 195465801
34 422064713
35 782586441
36 782586441
37 1688982089
38 3131929161
39 3131929161
40 6758904393
41 12530692681
42 12530692681
43 27038593609
44 50129261129
45 50129261129
46 108166435401
47 200529105481
48 200529105481
49 432677802569
50 802142540361
51 802142540361
52 1730759807561
53 3208618758729
54 3208618758729
55 6923087827529
56 12834580197961
57 12834580197961
58 27692546388553
59 51338515870281
60 51338515870281
61 110770380632649
62 205354484822601
63 205354484822601
64 443082304393801
65 821418721153609
66 821418721153609
67 1772329999438409
68 3285676572873289
69 3285676572873289
70 7089323128099401
71 13142709421838921
72 13142709421838921
73 28357295642743369
74 52570844443284041
75 52570844443284041
76 113429195098690121
77 210283390300852809
78 210283390300852809
79 453716792922477129
80 841133588239028809
81 841133588239028809
82 1814867221812679241
83 3364534403078885961
84 3364534403078885961
85 7259468937373487689
86 13458137720469918281
87 13458137720469918281
88 29037875950010995273
89 53832551082396717641
90 53832551082396717641
91 116151504000561025609
92 215330204762252612169
93 215330204762252612169
94 464606016804360524361
95 861320819851126870601
96 861320819851126870601
97 1858424068019558519369
98 3445283281135218692681
99 3445283281135218692681
100 7433696275286804238921
101 13781133127749444932169
102 13781133127749444932169
103 29734785104355787117129
104 55124532517920818958921
105 55124532517920818958921
106 118939140430257623503433
107 220498130084517750870601
108 220498130084517750870601
109 475756561733864969048649
110 881992520365763354792521
111 881992520365763354792521
112 1903026246986798196986441
113 3527970081514391739961929
114 3527970081514391739961929
115 7612104987998531108737609
116 14111880326168337145401929
117 14111880326168337145401929
118 30448419952199478498431561
119 56447521304878702645088841
120 56447521304878702645088841
121 121793679809003268057207369
122 225790085219957892102885961
123 225790085219957892102885961
124 487174719236834490168119881
125 903160340880652986350834249
126 903160340880652986350834249
127 1948698876948159378611769929
128 3612641363524384274620912201
129 3612641363524384274620912201
130 7794795507795923189331694153
131 14450565454100822773368263241
132 14450565454100822773368263241
133 31179182031186978432211391049
134 57802261816410380413470806601
135 57802261816410380413470806601
136 124716728124761056435137057353
137 231209047265654664360174719561
138 231209047265654664360174719561
139 498866912499057368446839722569
140 924836189062647014733211275849
141 924836189062647014733211275849
142 1995467649996282044625046245961
143 3699344756250640629770532459081
144 3699344756250640629770532459081
145 7981870599985180749337872339529
146 14797379025002675948264700809801
147 14797379025002675948264700809801
148 31927482399940933280729262494281
149 59189516100010914076436576375369
150 59189516100010914076436576375369
151 127709929599763943406294823113289
152 236758064400044110022526700261961
153 236758064400044110022526700261961
154 510839718399056614758740495864393
155 947032257600177281223668004459081
156 947032257600177281223668004459081
157 2043358873596227300168523186868809
158 3788129030400710939761843707744841
159 3788129030400710939761843707744841
160 8173435494384912565208445703590473
161 15152516121602847123581727787094601
162 15152516121602847123581727787094601
163 32693741977539653625368135770477129
164 60610064486411395753795798399095369
165 60610064486411395753795798399095369
166 130774967910158627959610155397452361
167 242440257945645596473320805911925321
168 242440257945645596473320805911925321
169 523099871640634525296578233905353289
170 969761031782582414931158973141652041
171 969761031782582414931158973141652041
172 2092399486562538155018863817501086281
173 3879044127130329713557186774446281289
174 3879044127130329713557186774446281289
175 8369597946250152673908006151884018249
176 15516176508521318970380250897829106249
177 15516176508521318970380250897829106249
178 33478391785000610910962228937122943561
179 62064706034085276096851207920903295561
180 62064706034085276096851207920903295561
181 133913567140002443859179120078078644809
182 248258824136341104852010847685857284681
183 248258824136341104852010847685857284681
184 535654268560009776298037299361325027913
185 993035296545364420269364209792439587401
186 993035296545364420269364209792439587401
187 2142617074240039106053470016494310560329
188 3972141186181457682935880906387200447049
189 3972141186181457682935880906387200447049
190 8570468296960156427659163345381749723721
191 15888564744725830735188806904953309270601
192 15888564744725830735188806904953309270601
193 34281873187840625714081936660931506377289
194 63554258978903322948188923891891471159881
195 63554258978903322948188923891891471159881
196 137127492751362502870108879768266900279881
197 254217035915613291806536828692106759410249
198 254217035915613291806536828692106759410249
199 548509971005450011494216652197608475890249
200 1016868143662453167255882099869574254596681
【讨论】:
是的,所以通常这类问题的问题不是直观地看到哪些术语可以忽略不计,而是以直截了当的方式严格论证。我同意你的说法,但这不是一个非常严格的答案 @ChrisBeck:没错,我正在尝试想出一种更好的方法来制定它。 @ChrisBeck:我至少证明了 O(4^(n log n)) 的上限,但我认为下排的权重会明显减少,你有什么灵感吗?进一步降低因子? 我有个问题,foo(a/2) 的循环也包含在 foo(a/2-3) 里面。我们也可以避免吗? @CommuSoft:我重写了我的答案,我想我找到了一个不错的方法【参考方案2】:(重写以给出更好的答案。)
这里有一个简单而严谨的分析,说明为什么T(n) ~ 4^n/3
是一个严格的估计。
我们有重复
T(n) = 4T(n-3) + 4T(n/2)
为了得到严格的结果,我们希望看到 T(n/2)
与 T(n-3)
相比可以忽略不计。我们可以这样做。
首先,T
对所有 n
都是非负数,所以特别是 T(n/2) >= 0
,所以对于所有 n
,我们有一个不等式,
T(n) >= 4T(n-3)
现在,我们要使用该不等式来比较 T(n-3)
和 T(n/2)
。
通过应用这种不平等n/6 - 1
次,我们得到了这一点
T(n-3) >= 4^n/6 - 1 * T(n/2)
(因为,(n/6 - 1) * 3 = n/2 - 3
和 n/2 - 3 + n/2 = n - 3
)。
这意味着T(n/2)
比T(n-3)
小:
T(n/2) <= 4^-n/6 + 1 * T(n-3)
现在,对于任何epsilon > 0
,都有一个n_0
,这样对于n > n_0
,4^-n/6 + 1 < epsilon
。 (因为4^-n/6 + 1
的限制是零,因为n
变大了。)
这意味着对于任何epsilon > 0
,都有足够大的n
,因此
4T(n-3) <= T(n) <= (4 + epsilon) T(n-3)
这会产生紧密绑定T(n) = 4^(n/3 + o(n))
。
获得更准确的估计
cmets 中有一些关于摆脱上面的o(n)
以获得更准确的估计的问题。
我担心这基本上只会变得迂腐——通常没有人关心低阶术语,而准确地确定它们只是一些微积分工作。但无论如何,我们今天可以做得更多。
有什么区别
首先,O(4^n/3)
和4^n/3 + o(n)
有什么区别? (或者,我们可以将后者写成(4+o(1))^n/3
。)
不同之处在于它们控制低阶项的程度。 O(4^n/3)
非常严格地控制它们——它表示您不会超过(具体)值 4^n/3)
超过一个常数因子。
4^n/3 + o(n)
,允许您超过4^n/3
超过一个常数因子。但这个因素在n
中是次指数的,与4^n/3
相比可以忽略不计。
例如,考虑函数f(n) = n * 4^n/3
。这个函数不是O(4^n/3)
。事实上,它超过了它一个因子n
,而不是一个常数因子。
但是,f(n)
属于 4^n/3 + o(n)
类。为什么?因为n = O(4^epsilon n)
对应每个epsilon > 0
。
当你遇到不等式时,
4T(n-3) <= T(n) <= (4 + epsilon) T(n-3)
对于每一个epsilon > 0
,你只能从这个T(n) = (4 + o(1))^n/3
推导出来。
为了获得更清晰的界限,我们需要将 epsilon
视为 n
的函数,而不是常量(就像我在惰性版本中所做的那样。)
证明
让epsilon(n) = 4^-n/6 + 1
在下面。然后我们已经展示了
T(n) <= (4 + epsilon(n)) T(n-3)
我们想看看T = O(4^n/3)
。
这可以扩展为迭代产品:
T(n) = PI_i=1^n/3 (4 + epsilon(3i))
我们可以对每一项进行分解,然后取出一个因数 4 得到
T(n) = 4^n/3 * PI_i=1^n/3 (1 + epsilon(3i)/4 )
现在的目标是证明
PI_i=1^n/3 (1 + epsilon(3i)/4 ) = O(1)
然后我们就完成了。
为此,我们获取日志,并显示为O(1)
。
SUM_i=1^n/3 log(1 + epsilon(3i/4))
我们使用log(1+x) <= x
绑定x >= 0
。
SUM_i=1^n/3 epsilon(3i/4)
现在我们使用 epsilon 的定义。事实上,对于一些C > 1
,我们只需要知道epsilon(n) <= C^-n
。上面变成了
SUM_i=1^n/3 C'^-i
对于一些常量C' > 1
。但这是一个几何级数,所以它以无穷几何级数为界为
1 / (1 - 1/C') = O(1)
因此T(n) = O(4^n/3)
。
因为我们已经有了T(n) = Omega(4^n/3)
,所以我们现在把它紧紧地固定在常量上,T(n) = Θ(4^n/3)
您可以自己决定这项额外的工作是否让事情变得更加清晰:p 就我个人而言,我通常更喜欢将 o(n)
留在那里。
【讨论】:
+/2-1/。我认为这绝对是最保守的(因此绝对是正确的方法)。 (那是加一,但显然不再允许在评论中使用)。o(n)
是不必要的。
@YvesDaoust:是的,但它需要更多的计算才能看到......通常没有人关心o
术语。只想要一个简单的论据,给出一个很好的估计。可以尝试消除o
术语,我想只是为了帮助学生了解如何做到这一点,但这是我认为的唯一原因。
顺便说一句,T(n)
不是'4^(n/3 + o(n))
,它是“O(4^(n/3 + o(n))
”(如果这个符号有意义的话),这是严格意义上的O(4^(n/3))
,因为n
吸收了@ 987654399@.【参考方案3】:
IMO,时间复杂度为Θ(r^n)
,其中r=³√4
。
确实,将这个表达式插入递归关系,
r^n = 1 + 4 r^n / r³ + 4 r^(n/2) = 1 + r^n + 4 √(r^n),
其中第二项渐近地占主导地位。
下面是对foo
的总调用次数除以r^n
的图表,便于阅读。我们假设了f(n/2)
中的楼层[n/2]
。
比率倾向于重复序列46.6922952502
,63.4656065932
74.1193985991
。这似乎证实了Θ(r^n)
。
更新:
通过归纳,我们可以证明对于n >= 21
,
T(n) < B(n) = 75.(s^(2n) - 4.s^n),
s=³√2
.
确实,通过递归方程和归纳假设,
T(n+3) = 1 + 4.T(n) + 4.T([(n+3)/2])
< 1 + 4.75.(s^(2n) - 4.s^n) + 4.75.(s^(2[(n+3)/2])) - 4.s^[(n+3)/2])
我们将此与绑定的B(n+3)
进行比较以建立
1 + 4.75.(s^(2n) - 4.s^n) + 4.75.(s^(2[(n+3)/2])) - 4.s^[(n+3)/2])
< 75.(s^(2n+6) - 4.s^[(n+3)/2]
我们可以简化术语4.75.s^(2n)
并除以300.s^n
:
s^(-n)/300 - 4 + s^(-(n+3)%2) - 4.s^([(n+3)/2]-n) < - s^([(n+3)/2]-n)
或
s^(-n)/300 + s^(-(n+3)%2) < 4 + 5.s^([(n+3)/2]-n).
对于任何n
,这个不等式都成立,所以T(n) < B(n) => T(n+3) < B(n+3)
。
现在对于基本情况,我们使用@CommuSoft 给出的T(n)
表(并独立检查)并进行数字验证
T(21) = 744009 < 75.(s^42 - 4.s^21) = 1190400
T(22) = 1622601 < 75.(s^44 - 4.s^22) = 1902217.444...
T(23) = 3015241 < 75.(s^46 - 4.s^23) = 3035425.772...
...
T(41) = 12530692681 < 75.(s^82 - 4.s^41) = 12678879361
这表明归纳步骤可以从n=39
开始应用([(39+3)/2]=21
)。
然后
T(n) = O(75.(s^(2n) - 4.s^n)) = O(r^n).
(实际上,对于所有n >= 23
,46.r^n < T(n) < 75.r^n
,这非常紧;T(n) = Θ(r^n)
。)
【讨论】:
我不知道第一项渐近支配的事实是否足够。我可以想象场景就像谐波序列的总和,尾巴会长到足以产生重大影响。 好吧,您可以说,您创建了log n
的此类进程堆叠在一起,因为对于每一半,您仍然会生成大致等效的序列调用。
不,您需要将其视为一个矩阵。第一个进程序列是 n,n-3, n-6,... 1,因此是 n/3 个进程。这些过程中的每一个都将创建 n/2、n-3/2、...、1 个过程的“第二”层,因此在 2d 矩阵中(尽管在右侧有部分被切除,因为序列会更短)。现在这些将再次创建进程,...
在我看来,OP 的公式忘记考虑一些事情:每次调用的固定成本,所以成本应该是 T(n)=1+T(n-3)+T(n/2)
。
如果我添加一个对数因子,我大致得到相同的情节。如果您以对数比例绘制它,您会看到r^n
的小差异,这意味着(非常长期)存在差异。我确实添加了一个不允许的因素n
。界限大概是r^n*log(n)
以上是关于非常复杂的递归代码的时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章