分治策略(Divide and Conquer)是一种常用的算法设计技术,使用分治策略设计的算法通常是递归算法.
设 P P P 是待求解的问题, ∣ P ∣ |P| ∣P∣ 代表该问题的输入规模,一般的分治算法的伪代码描述如下:
- 要有终止条件,不然就会无限制的划分子问题,直至报错
- 终止条件不是说直接得到结果,而是子问题的求解已经非常简单,不需要在继续划分了,可以直接求解
- 划分子问题要注意子问题间相互独立
- 综合子问题的解不一定比求解子问题简单,不要在这一步丧失信心!
分治算法通常都是递归算法,这种算法的时间复杂度粉色系通常需要求解递推方程. 如果原问题的输入规模是
n
n
n,根据上面的伪码,分治算法时间复杂度的递推方程的一般形式是:
{
W
(
n
)
=
W
(
∣
P
1
∣
)
+
W
(
∣
P
2
∣
)
+
⋯
+
W
(
∣
P
k
∣
)
+
f
(
n
)
W
(
c
)
=
C
\begin{cases} W(n)=W(|P_1|)+W(|P_2|)+\cdots+W(|P_k|)+f(n) \\ \\ W(c)=C \end{cases}
⎩⎪⎨⎪⎧W(n)=W(∣P1∣)+W(∣P2∣)+⋯+W(∣Pk∣)+f(n)W(c)=C
上面的
C
C
C 代表直接求解规模为
c
c
c 的子问题的工作量,而
f
(
n
)
f(n)
f(n) 代表将原问题归约为若干子问题以及将子问题的解综合为原问题的解所需要的总工作量.
如果子问题的规模都一样,方程的求解比较简单,时间复杂度相对也比较低
所以,分治算法的核心就是如何划分子问题,以及划分的正确性的证明!
【例 1】有 n n n 片芯片,已知其中好芯片比坏芯片最少多 1 1 1 片. 现在需要通过测试从中找出 1 1 1 片好芯片. 测试的方法是:将 2 2 2 片芯片放到测试台上, 2 2 2 片芯片互相测试并报告测试结果:“好”或者 “坏”. 假定好芯片的报告是正确的,坏芯片的报告是不可靠的(可能是对的,也可能是错的). 请设计一个算法,使用最少的测试次数来找出 1 1 1 片好芯片.
【解】
-
由已知想可知
-
好芯片个数 + 坏芯片个数 = n n n
-
好芯片个数 ≥ 坏芯片个数 + 1
-
测试集结果枚举,假设芯片 A 和芯片 B 互相测试,则有:
A 报告 B 报告 结论 1 B 是好的 A 是好的 A、B 都好或者 A、B 都坏 2 B 是好的 A 是坏的 A、B 至少一个是坏的 3 B 是坏的 A 是好的 A、B 至少一个是坏的 4 B 是坏的 A 是坏的 A、B 至少一个是坏的
-
-
蛮力算法
挨个测试,只要有一个芯片得到 ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2} \rfloor ⌊2n⌋ 及以上的报告是好的,就可以得出结论,该芯片是好芯片。为什么呢?因为好芯片是超过一半的,即使 ⌊ n 2 ⌋ − 1 \displaystyle\lfloor \frac{n}{2} \rfloor-1 ⌊2n⌋−1 个坏芯片报告它是好的,还有一个好芯片的测试报告,它判断也是好的,说明真的是好的.
同理, ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2} \rfloor ⌊2n⌋ 及以上的报告是坏的,也可判定该芯片是坏的.
最坏情况下,你把坏芯片找完了才找到好芯片(并且人品相当差,每一轮测试,能下结论的第 ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2} \rfloor ⌊2n⌋ 个结论总是最晚出现),此时时间复杂度为 O ( n 2 ) O(n^2) O(n2)
-
优化算法
蛮力算法虽然简单,但是执行耗费的资源太大. 人都是倾向于懒惰的,既然相互测试避免不了,当然想着减少测试的次数了.
之前分析过,至少要得到 ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2} \rfloor ⌊2n⌋ 及以上的报告是好的(或坏的)才能得出结论该芯片是好的(或坏的),对于规模 n n n,测试的次数就是 Θ ( n 2 ) \Theta(n^2) Θ(n2) 这个量级,所以,要降低复杂度,应该着手去减小规模 n n n. 对于当前问题,显然是每次测试时通过根据其测试结论进行丢弃来达到减小规模的目的.
考虑分治算法,这里分组显然就是两两一组互检(因为题目给定了就这么检测,又不是三个一起互检),那么每组如何丢弃芯片来减小规模呢?
这里要注意一点,我们能判断出芯片是好的的前提是好的芯片比坏的芯片至少多一片!
那么,再结合上面列出的结果表可以分为两种情况:
- A、B 都好或者 A、B 都坏:丢弃一片,保留一片
- A、B 至少一个是坏的:全部丢弃
为什么可以这样呢?
当 n n n 是偶数时,设 A、B 都是好芯片的有 i i i 组,A、B 一好一坏的有 j j j 组,A、B 都坏的有 k k k 组,那么
2 i + 2 j + 2 k = n 2i+2j+2k=n 2i+2j+2k=n且有 2 i + j > 2 k + j ⇒ i > k 2i+j>2k+j\Rightarrow i>k 2i+j>2k+j⇒i>k
经过淘汰后,剩下的好芯片数为 i i i,坏芯片数至多为 k k k,满足 i > k i>k i>k但是当 n n n 是奇数,没被分组而轮空的是 1 1 1 片坏芯片时,可能出现淘汰后剩下的好芯片数与坏芯片数相等,对于奇数的情况,可以增加一轮特殊处理,将其按照蛮力法的判断方法,和其他芯片都检测一次,得出芯片的好坏,如果它是好的,算法结束,如果它是坏的,丢弃它.
这些额外的工作需要 O ( n ) O(n) O(n) 次测试,而分组内的测试也需要 O ( n ) O(n) O(n) 次(精确说是 ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2}\rfloor ⌊2n⌋ 次),因此,不管是奇数还是偶数,规约子问题的工作量都是 O ( N ) O(N) O(N)
由于每组至少丢弃掉 1 1 1 片芯片,剩下的芯片数至多 ⌊ n 2 ⌋ \displaystyle\lfloor \frac{n}{2}\rfloor ⌊2n⌋.
该算法的伪码描述如下:
如果这个世界总是这么简单就好了。
【例 2】设 a a a 是一个给定实数,计算 a n a^n an,其中 n n n 为自然数
【解】
如果使用蛮力算法,算法的时间复杂度是 O ( n ) O(n) O(n).
下面考虑分治算法. 将
a
n
a^n
an 看作两部分幂的乘积,每部分都是一个子问题,即
a
n
2
\displaystyle a^{\frac{n}{2}}
a2n 幂. 更确切的说,有:
a
n
=
{
a
n
2
×
a
n
2
n
为
偶
数
a
n
−
1
2
×
a
n
−
1
2
×
a
n
为
奇
数
\displaystyle a^n=\begin{cases} a^{\frac{n}{2}} \times a^{\frac{n}{2}} \quad \quad \quad \quad n 为偶数\\ a^{\frac{n-1}{2}} \times a^{\frac{n-1}{2}} \times a \quad n为奇数 \end{cases}
an={a2n×a2nn为偶数a2n−1×a2n−1×an为奇数
至此,我们可以得出分治策略的设计要点:
- 子问题与原问题具有相同的性质(方便用同样的算法来求解,如递归)
- 子问题的求解彼此独立
- 划分子问题的规模尽可能均衡
我不知道有没有人和我一样看分治的时候有点蒙圈,那就是分组和代码之间是反的,分组是从大到小分,代码时通过分治只用写子问题的解决代码