分治策略学习笔记
以下的东西来自中国大学MOOC北京大学屈婉玲老师的算法设计与分析课程,本人只是做了一下搬运,中间如果有什么错误,纯属本人的失误。慕课地址
分治策略的设计思想
将一个大问题分割成多个子问题,对子问题逐个求解,最终将所有子问题的结果汇总起来,得到大问题的解。
分治策略的一般性描述
Devide-and-Conquer( P )
- if |P| <= c then S( P )
- divide P into P 1 P_1 P1, P 2 P_2 P2, …, P k P_k Pk
- for
i
←
1
i←1
i←1 to k
. y i ← y_i← yi←Divide-andConquer( P i P_i Pi) - Return Merge( y 1 , y 2 , y 3 y_1, y_2, y_3 y1,y2,y3 …, y k y_k yk)
其中,第一步,判断当前问题P的规模能否直接进行求解,如果可以就直接进行求解。即当问题P的规模小于c时,使用方法S()对问题求解。
第二步,将问题P分割为k个规模更小的子问题。
第三步,逐个对问题P分割出来的更小的子问题
P
k
P_k
Pk进行求解,这是一个递归调用的过程。
第四步,将所有的子问题的解合并,返回最终结果。
设计要点
1、原问题可以划分或者规约为规模较小的子问题。其中,子问题要与原问题有相同的性质;子问题的各个解之间彼此独立;划分时子问题的规模要尽可能的均衡。
2、子问题的规模小到一定的程度的时候可以直接进行求解。
3、子问题的解综合之后可以得到原问题的解。
在分治策略中有以下几个问题:
1、子问题 P i P_i Pi划分成多大规模的时候可以进行求解,这个标准是多少。
2、子问题 P i P_i Pi如果可以进行求解了,怎样进行求解,要有一个通用的方法,能够得到正确的答案。
3、问题P如何进行分割,标准是什么。
4、问题P在得到其所有子问题的解之后,如何进行合并得到P的解。
例题
芯片测试
有N个芯片,其中好的芯片总要比坏的芯片至少多一片。
好的芯片测试结果一定是正确的,坏的芯片测试结果不确定,可能是好的也可能是坏的。
那么对于一次芯片测试,有以下的测试结果
A报告 | B报告 | 结论 |
---|---|---|
B是好的 | A是好的 | A/B都是好的或者都是坏的 |
B是好的 | A是坏的 | A/B至少有一片是坏的 |
B是坏的 | A是好的 | A/B至少有一片是坏的 |
B是坏的 | A是坏的 | A/B至少有一片是坏的 |
问题
输入:N片芯片,好的芯片至少要比坏的多一片。
问题:设计一种方法,从N个芯片中找到一个好的芯片。
要求:使用最少的次数。
分析:
在输入n为奇数时,至少有
(
n
+
1
)
2
\frac {(n+1)}{2}
2(n+1)片芯片是好的,有
(
n
−
1
)
2
\frac {(n-1)}{2}
2(n−1)片芯片是坏的。
然后取其中一片芯片依次和其他的芯片进行测试。如果这个芯片是好的,那么剩下的好的芯片就只有
(
n
−
1
)
2
\frac {(n-1)}{2}
2(n−1)片,测试结果也就是至少会有
(
n
−
1
)
2
\frac {(n-1)}{2}
2(n−1)个测试结果是好。如果芯片是坏的,那么剩余的芯片中就会有
(
n
+
1
)
2
\frac {(n+1)}{2}
2(n+1)个好芯片。然后就会有至少
(
n
+
1
)
2
\frac {(n+1)}{2}
2(n+1)个测试结果是坏。根据这个就可以判断出来这个芯片是好还是坏。
当输入为偶数时,至少会有
n
2
+
1
\frac {n}{2}+1
2n+1片芯片是好的,然后同上面的逻辑,就又可以得到这个芯片是好还是坏。
另外就是,将所有的芯片两两进行对比,只取结测试结果均为好的一次测试中的一枚芯片,这样可以保证取到的结果集中,扔可以保证好的芯片的数量至少比坏的芯片多一。
蛮力算法
直接取一个芯片同剩余的芯片进行对比,若这枚芯片为好的,就返回;不好就将这片芯片扔掉,继续进入下一次的判断,直到得到所有的结果。
时间复杂度为 O ( n 2 ) O(n^2) O(n2)
分治算法
如果n的数量为偶数,就两两一组进行对比,只取结果均为好的那一组中的一个。
如果n为奇数,仍然两两一组进行对比,对轮空的那个单独进行对比。
这样直到n的数量小于等于3,
n=3,任取其中一组进行对比,如果结果均为好,那就随便返回其中的一个,如果不是,就返回还未对比的那个。
n<3,任取一个返回结果。
快速排序
基本思想
使用首元素x作为基准,将数组分为比x大的,比x小的两部分。x就在他们中间,然后依次对剩余的两部分进行递归调用,最后就得到结果了。
其中在最好的情况下,也就是每次都是均分数组时间复杂度为
O
(
n
log
2
n
)
O(n\log _2 n)
O(nlog2n)。
最坏的情况下,每次都是0/n-1进行划分,这样时间复杂度为
O
(
n
(
n
−
1
)
2
)
O(\frac {n(n-1)}{2})
O(2n(n−1))。
另外,只要是按照某个特定比例进行划分,时间复杂度仍然为
O
(
n
log
2
n
)
O(n\log _2 n)
O(nlog2n)。
平均时间复杂度为 O ( n log 2 n ) O(n\log _2 n) O(nlog2n)。
幂乘问题
正常情况下
a
n
=
a
×
a
×
⋯
×
a
a^n=a \times a \times \cdots \times a
an=a×a×⋯×a
这一共乘了
n
n
n次,自然而然,时间复杂度为
O
(
n
)
O(n)
O(n)
使用分治算法之后
a
n
=
{
a
n
2
×
a
n
2
n
为偶数
a
n
−
1
2
×
a
n
−
1
2
×
a
n
为奇数
a^n = \begin {cases} {a^{\frac{n}{2}} \times a^{\frac{n}{2}}}& \text{$n$为偶数}\\\\ {a^{\frac{n-1}{2}} \times a^{\frac{n-1}{2}} \times a}& \text{$n$为奇数} \end{cases}
an=⎩⎪⎨⎪⎧a2n×a2na2n−1×a2n−1×an为偶数n为奇数
这个时候子问题的规模就变成
n
2
\frac{n}{2}
2n了,而分成的两个子问题又一样,只需要计算一次即可。
整个算法的时间复杂度为 O ( log 2 n ) O(\log_2 n) O(log2n)
Fibonacci数列
对于Fibonacci数列有以下地推公式:
F
n
=
F
n
−
1
+
F
n
−
2
F_n = F_{n-1}+F_{n-2}
Fn=Fn−1+Fn−2
按照这个公式设计算法,时间复杂度为 O ( n ) O(n) O(n)
对于Fibonacci数列又有以下的定理:
[
F
n
+
1
F
n
F
n
F
n
−
1
]
=
[
1
1
1
0
]
n
\begin{bmatrix} F_{n+1} & F_{n} \\ F_{n} & F_{n-1} \end{bmatrix} = {\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}}^n
[Fn+1FnFnFn−1]=[1110]n
具体的证法自己去百度,用的是归纳法。
使用这个公式,就可以在 O ( log 2 n ) O(\log_2 n) O(log2n)次矩阵运算后得到结果。每次矩阵运算要进行8次乘法计算,所以整个算法的时间复杂度为 O ( log 2 n ) O(\log_2 n) O(log2n)
对分治算法的优化
分治算法的时间复杂度方程:
W
(
n
)
=
a
W
(
n
b
)
+
d
(
n
)
W(n) = aW( \frac{n}{b}) + d(n)
W(n)=aW(bn)+d(n)
其中:
- a a a:子问题个数
- n b \frac{n}{b} bn子问题规模
- d ( n ) d(n) d(n):划分与综合的工作量
而对分治算法的优化也主要集中在对 a a a的减小或者是对 d ( n ) d(n) d(n)的减小上。
减少子问题个数
当 a a a较大, b b b较小, d ( n ) d(n) d(n)不大时,方程的解为: W ( n ) = O ( n log b a ) W(n)=O(n^{\log_b a}) W(n)=O(nlogba)。
由此可以看出,减少子问题的个数是降低 W ( n ) W(n) W(n)的复杂度的一种途径。
主要方法就是通过问题之间的依赖关系,使子问题的解可以通过组合其他子问题的解得到。
增加预处理
这个方法就是通过提前对输入集进行处理,将每一个子问题都需要进行的处理提出来,先进行处理,这样就减小了 d ( n ) d(n) d(n)借此,减小算法的时间复杂度。