赞
踩
分治是一个简单但是非常有效的算法,它的思想也很简单,就是将原问题分成几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再将这些问题的结果合并,就得到的原问题的解。
分治法每层的递归都有三个步骤:
本篇文章就介绍几个经典的应用分治算法的例子,并且附带练习题
本文会有很多算法的时间复杂度需要解递归,这里先将主定理给出,之后我会专门写一篇文章介绍接递归的各种方法
主定理只能应用于形如 T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)的递归式,并且其中 a ≥ 1 , b > 1 , 对于 f ( n ) , ∃ n 0 ∈ R , s . t . n > n 0 时, f ( n ) > 0 a\ge1,b>1,对于f(n),\exists n_0 \in R,s.t. \space n>n_0时,f(n)>0 a≥1,b>1,对于f(n),∃n0∈R,s.t. n>n0时,f(n)>0
时间复杂度通过比较 f ( n ) f(n) f(n)和 n l o g b a n^{log_b^a} nlogba
C a s e 1 : Case1: Case1:当 f ( n ) = O ( n l o g b a − ε ) , ε > 0 f(n)=O(n^{log_b^a-\varepsilon}),\varepsilon>0 f(n)=O(nlogba−ε),ε>0, T ( n ) = Θ ( n l o g b a ) T(n)=\Theta(n^{log_b^a}) T(n)=Θ(nlogba)
C a s e 2 : Case2: Case2:当 f ( n ) = Θ ( n l o g b a ⋅ lg k + 1 a ) , k ≥ 0 f(n)=\Theta(n^{log_b^a}\cdot \lg^{k+1}_{}{a}),k\ge 0 f(n)=Θ(nlogba⋅lgk+1a),k≥0, T ( n ) = Θ ( n l o g b a ⋅ lg k a ) T(n)=\Theta(n^{log_b^a}\cdot \lg^{k}_{}{a}) T(n)=Θ(nlogba⋅lgka)
C a s e 3 : Case3: Case3:当 f ( n ) = Ω ( n l o g b a + ε ) , ε > 0 f(n)=\Omega (n^{log_b^a+\varepsilon}),\varepsilon>0 f(n)=Ω(nlogba+ε),ε>0, T ( n ) = Θ ( f ( n ) ) T(n)=\Theta(f(n)) T(n)=Θ(f(n))
归并排序的思路也很简单
实现代码:
void part_sort(int* arr, int l, int m, int r) { int n1 = m - l + 1; int n2 = r - m; int* L = new int[n1+1]; int* R = new int[n2+1]; //将[l,m],[m+1,r]分别拷贝到两个新数组中 for (int i = 0; i < n1; ++i)L[i] = arr[l + i]; for (int i = 0; i < n2; ++i)R[i] = arr[m + i + 1]; L[n1] = INT_MAX, R[n2] = INT_MAX;//将两个数组的末尾赋为INT_MAX作为哨兵位,可以简化代码 //按降序将两个数组合并 //如果排升序就要改变哨兵位的值位INT_MIN int i = 0, j = 0; for (int k = l; k <= r; ++k) { if (L[i] > R[j])arr[k] = R[j++]; else arr[k] = L[i++]; } } void merge_sort(int* arr, int l,int r) { if (l >= r)return; //分解 int mid = (l + r) / 2; //递归 merge_sort(arr, l, mid); merge_sort(arr, mid + 1, r); //排序 part_sort(arr, l, mid, r); }
下面我们分析一下归并排序的时间复杂度,有人会说,这很简单呀,不就是 O ( n l o g n ) O(nlogn) O(nlogn)嘛,但是它是怎么怎么推出来的,大多数人学这个算法应该是通过画递归树来求得的(反正我第一次学是这样),这里有更严谨的方法
首先分析一下算法的消耗,递归的时间消耗是
2
T
(
n
/
2
)
2T(n/2)
2T(n/2),就是分成左右两个
n
/
2
n/2
n/2的子序列嘛,之后是排序每个子序列的消耗是
Θ
(
n
)
\Theta(n)
Θ(n),所以就能得到这个时间消耗的表达式
T
(
n
)
=
{
Θ
(
1
)
n
=
1
2
T
(
n
/
2
)
+
Θ
(
n
)
n
>
1
T(n)=\left\{\begin{matrix} \Theta(1) & n=1\\ 2T(n/2)+\Theta(n)&n>1 \end {matrix}\right.
T(n)={Θ(1)2T(n/2)+Θ(n)n=1n>1
再由主定理解这个递归式,
f
(
n
)
=
Θ
(
n
l
o
g
b
a
l
g
k
n
)
f(n)=\Theta(n^{log_{b}^a}lg^kn)
f(n)=Θ(nlogbalgkn),是情况二,且
k
=
0
k=0
k=0,就得出
T
(
n
)
=
Θ
(
n
l
o
g
b
a
l
g
k
+
1
n
)
=
Θ
(
n
l
o
g
n
)
T(n)=\Theta(n^{log_{b}^a}lg^{k+1}n)=\Theta(nlogn)
T(n)=Θ(nlogbalgk+1n)=Θ(nlogn)。归并排序是一个比较基础的排序,但是也很重要,这里给一道归并排序的练习题:习题
之前的文章已经讲过了 详解快速幂(附习题)
斐波那契数列应该没人不知道吧,以防万一我先把斐波那契数列的定义写下:
F
i
=
{
0
i
=
0
1
i
=
1
F
i
−
1
+
F
i
−
2
i
>
1
F_i= \left\{
求斐波那契数列有很多方法,下面我们一一介绍
直接递归求解,将 F i F_i Fi分成 F i − 1 F_{i-1} Fi−1和 F i − 2 F_{i-2} Fi−2,当 i = 0 i=0 i=0或 i = 1 i=1 i=1时分别返回 0 0 0和 1 1 1
int fib(int n) {
if (n == 0) return 0;
if (n == 1)return 1;
return fib(n - 1) + fib(n - 2);
}
这个方法很简单,但缺点也很明显时间复杂度太高,是指数级的 O ( ϕ n ) O(\phi ^n) O(ϕn)( ϕ \phi ϕ是黄金分割率)
int fib(int n) {
int* F = new int[n+1];
F[0] = 0, F[1] = 1;
for (int i = 2; i <= n; ++i) F[i] = F[i - 1] + F[i - 2];
int res=F[n];
delete[] F;
return res;
}
时间复杂度是线性的 O ( n ) O(n) O(n),空间复杂度也是 O ( n ) O(n) O(n),可以将数组该成三个变量来递推也可以,空间复杂度就成 O ( 1 ) O(1) O(1)了
这里涉及二阶常系数线性齐次差分方程的求解,不了解的可以先了解一下这方面的知识
由斐波那契数列的递推式
F
i
=
F
i
−
1
+
F
i
−
2
F_i=F_{i-1}+F_{i-2}
Fi=Fi−1+Fi−2,可以得到差分方程
F
i
−
F
i
−
1
−
F
i
−
2
=
0
F_i-F_{i-1}-F_{i-2}=0
Fi−Fi−1−Fi−2=0,得到其特征方程,
x
2
−
x
−
1
=
0
x^2-x-1=0
x2−x−1=0,由此得到两个解
λ
1
=
1
+
5
2
λ
2
=
1
−
5
2
有两个不同的实根,所以有通解
F
i
=
C
1
λ
1
n
+
C
2
λ
2
n
F_i=C_1 \lambda_1^n+C_2\lambda_2^n
Fi=C1λ1n+C2λ2n,将初值条件
F
1
=
1
,
F
0
=
0
F_1=1,F_0=0
F1=1,F0=0带入,就可以得到
F
i
=
λ
1
i
−
λ
2
i
5
F_i=\frac{\lambda_1^i-\lambda_2^i }{\sqrt{5} }
Fi=5
λ1i−λ2i
这样我们就只需求两个根的
n
n
n次方即可,时间复杂度就可以降到和快速幂一样的情况
O
(
l
o
g
n
)
O(logn)
O(logn)
int fib(int n) {
double sq5 = sqrt(5);
double fi1 = (1 + sq5) / 2;
double fi2 = (1 - sq5) / 2;
return (pow(fi1, n) - pow(fi2, n)) / sq5;
}
矩阵幂公式
[
F
i
+
1
F
i
F
i
F
i
−
1
]
=
[
1
1
1
0
]
i
证明:
b
a
s
e
:
base:
base:
[
F
2
F
1
F
1
F
0
]
=
[
1
1
1
0
]
s
t
e
p
:
step:
step:
[
F
i
+
1
F
i
F
i
F
i
−
1
]
=
[
F
i
F
i
−
1
F
i
−
1
F
i
−
2
]
[
1
1
1
0
]
[
F
i
+
1
F
i
F
i
F
i
−
1
]
=
[
F
i
−
1
F
i
−
2
F
i
−
2
F
i
−
3
]
[
1
1
1
0
]
2
以此类推,继续把矩阵拆开,最终右边就只剩初始矩阵的
i
i
i次方
[
F
i
+
1
F
i
F
i
F
i
−
1
]
=
[
1
1
1
0
]
i
这样也可以将斐波那契的求解转换成求幂次的形式,也和快速幂的时间复杂度相同
O
(
l
o
g
n
)
O(logn)
O(logn)
int** multiply(int** a, int** b) { //两矩阵相乘,因为求斐波那契数列只需要2*2的矩阵,所以就直接用朴素算法了算了 int** c = new int* [2]; for (int i = 0; i < 2; ++i)c[i] = new int[2] {0, 0}; c[0][0] = a[0][0] * b[0][0] + a[0][1] * b[1][0]; c[0][1] = a[0][0] * b[0][1] + a[0][1] * b[1][1]; c[1][0] = a[1][0] * b[0][0] + a[1][1] * b[1][0]; c[1][1] = a[1][0] * b[0][1] + a[1][1] * b[1][1]; return c; } int** matrix_pow(int** mat, int n) {//矩阵快速幂 int** res = new int* [2]; for (int i = 0; i < 2; ++i)res[i] = new int[2] {0, 0}; res[0][0] = 1, res[1][1] = 1; while (n > 0) { if (n & 1) res=multiply(res, mat); mat=multiply(mat, mat); n >>= 1; } return res; } int fib(int n) { int** mat = new int* [2]; for (int i = 0; i < 2; ++i) mat[i] = new int[2] {1, 1}; mat[1][1] = 0; mat = matrix_pow(mat, n+1); return mat[1][1]; }
学完一定要及时练习一下这些方法:斐波那契数列练习
两个矩阵相乘如果用朴素算法(就是按乘法规则一个一个元素的乘起来)的话时间复杂度是
O
(
n
3
)
O(n^3)
O(n3),效率不是很令人满意,该如何优化呢?本文章是介绍分治,所以优化一定是用分治法来优化,分治和矩阵联系起来,可以想到什么?一定是矩阵分块,我们可以将
n
×
n
n\times n
n×n的矩阵分成由
n
/
2
×
n
/
2
n/2\times n/2
n/2×n/2块矩阵`组成的
2
×
2
2\times 2
2×2矩阵
[
c
11
c
12
…
c
1
n
c
21
c
22
…
c
2
n
⋮
⋮
⋮
c
n
1
c
n
2
…
c
n
n
]
=
[
a
11
a
12
…
a
1
n
a
21
a
22
…
a
2
n
⋮
⋮
⋮
a
n
1
a
n
2
…
a
n
n
]
⋅
[
b
11
b
12
…
b
1
n
b
21
b
22
…
b
2
n
⋮
⋮
⋮
b
n
1
b
n
2
…
b
n
n
]
↓
\downarrow
↓
[
r
s
t
u
]
=
[
a
b
c
d
]
⋅
[
e
f
g
h
]
其中
{
r
=
a
e
+
b
g
s
=
a
f
+
b
h
t
=
c
e
+
d
g
u
=
c
f
+
d
h
\left\{
但是这样子真的优化了矩阵乘法嘛?我们可以分析一下这个算法的时间复杂度,因为有
8
8
8对矩阵相乘,所以递归时间消耗的系数是
8
8
8,每层递归将矩阵划分为
n
/
2
n/2
n/2的块矩阵,所以递归的时间消耗是
8
T
(
n
/
2
)
8T(n/2)
8T(n/2),加上将这些矩阵相加的时间消耗,可以得到
T
(
n
)
=
8
T
(
n
/
2
)
+
Θ
(
n
2
)
T(n)=8T(n/2)+\Theta(n^2)
T(n)=8T(n/2)+Θ(n2)
根据主定理,
Θ
(
n
2
)
=
O
(
n
l
o
g
2
8
)
\Theta(n^2)=O(n^{log_2^8})
Θ(n2)=O(nlog28),情况一,所以时间复杂度
T
(
n
)
=
Θ
(
n
3
)
T(n)=\Theta(n^3)
T(n)=Θ(n3),啊?优化了个寂寞。虽然没有优化,但是这个递归式给了我们优化的思路,我们可以想办法降低递归的时间消耗,就是减少计算矩阵乘法的次数或是改变分块的方式,改变分块的方式显然是行不通的,因为你将一个块矩阵分的小一些,其他块矩阵一定会变大,相当于没怎么变,那就只有减少计算矩阵乘法的次数了。
下面给出的算法非常神奇,一般人真的想不到
算法主要想法就是减少矩阵乘法,为此给出了七个式子
P
1
=
a
(
f
−
h
)
P
2
=
(
c
+
b
)
h
P
3
=
(
c
+
d
)
e
P
4
=
d
(
g
−
3
)
P
5
=
(
a
+
d
)
(
e
+
h
)
P
6
=
(
b
−
d
)
(
g
+
h
)
P
7
=
(
a
−
c
)
(
e
+
f
)
通过这七个式子我就可以表示
r
,
s
,
t
,
u
r,s,t,u
r,s,t,u
r
=
P
5
+
P
4
−
P
2
+
P
6
s
=
P
1
+
P
2
t
=
P
3
+
P
4
u
=
P
5
+
P
1
−
P
3
−
P
7
这里我就不证明了,如果你有兴趣可以推导试试看,通过这样的构造,每层递归就只需要计算
7
7
7个矩阵乘法,优化后
T
(
n
)
=
7
T
(
n
/
2
)
+
Θ
(
n
2
)
T(n)=7T(n/2)+\Theta(n^2)
T(n)=7T(n/2)+Θ(n2),在根据主定理就可以得到,
T
(
n
)
=
Θ
(
n
l
g
7
)
=
Θ
(
n
2.87
)
T(n)=\Theta(n^{lg7})=\Theta(n^{2.87})
T(n)=Θ(nlg7)=Θ(n2.87)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。