当前位置:   article > 正文

分治法及应用(附练习)_离散数学分治算法例题

离散数学分治算法例题

基本思想

分治是一个简单但是非常有效的算法,它的思想也很简单,就是将原问题分成几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再将这些问题的结果合并,就得到的原问题的解。
分治法每层的递归都有三个步骤:

  1. 分解
  2. 递归
  3. 合并

本篇文章就介绍几个经典的应用分治算法的例子,并且附带练习题

本文会有很多算法的时间复杂度需要解递归,这里先将主定理给出,之后我会专门写一篇文章介绍接递归的各种方法
主定理只能应用于形如 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 a1,b>1,对于f(n)n0Rs.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)=Θ(nlogbalgk+1a)k0 T ( n ) = Θ ( n l o g b a ⋅ lg ⁡ k a ) T(n)=\Theta(n^{log_b^a}\cdot \lg^{k}_{}{a}) T(n)=Θ(nlogbalgka)
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))

归并排序

归并排序的思路也很简单

  1. 分解:分解待排序的 n n n个元素分成两个具有 n / 2 n/2 n/2个元素的子序列
  2. 递归:递归将各个子序列排序
  3. 合并:合并已经拍好的子序列

实现代码:

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

下面我们分析一下归并排序的时间复杂度,有人会说,这很简单呀,不就是 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\{

0i=01i=1Fi1+Fi2i>1
\right. Fi= 01Fi1+Fi2i=0i=1i>1
求斐波那契数列有很多方法,下面我们一一介绍

1.递归求解

直接递归求解,将 F i F_i Fi分成 F i − 1 F_{i-1} Fi1 F i − 2 F_{i-2} Fi2,当 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5

这个方法很简单,但缺点也很明显时间复杂度太高,是指数级的 O ( ϕ n ) O(\phi ^n) O(ϕn) ϕ \phi ϕ是黄金分割率)

2.线性dp
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

时间复杂度是线性的 O ( n ) O(n) O(n),空间复杂度也是 O ( n ) O(n) O(n),可以将数组该成三个变量来递推也可以,空间复杂度就成 O ( 1 ) O(1) O(1)

3.公式法

这里涉及二阶常系数线性齐次差分方程的求解,不了解的可以先了解一下这方面的知识

由斐波那契数列的递推式 F i = F i − 1 + F i − 2 F_i=F_{i-1}+F_{i-2} Fi=Fi1+Fi2,可以得到差分方程 F i − F i − 1 − F i − 2 = 0 F_i-F_{i-1}-F_{i-2}=0 FiFi1Fi2=0,得到其特征方程, x 2 − x − 1 = 0 x^2-x-1=0 x2x1=0,由此得到两个解
λ 1 = 1 + 5 2 λ 2 = 1 − 5 2

λ1=1+52λ2=152
λ1=21+5 λ2=215
有两个不同的实根,所以有通解 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
4.矩阵幂

矩阵幂公式
[ F i + 1 F i F i F i − 1 ] = [ 1 1 1 0 ] i

[Fi+1FiFiFi1]
=
[1110]
^i [Fi+1FiFiFi1]=[1110]i
证明:
b a s e : base: base:
[ F 2 F 1 F 1 F 0 ] = [ 1 1 1 0 ]
[F2F1F1F0]
=
[1110]
[F2F1F1F0]=[1110]

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 ]
[Fi+1FiFiFi1]
=
[FiFi1Fi1Fi2]
[1110]
[Fi+1FiFiFi1]=[FiFi1Fi1Fi2][1110]

[ 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
[Fi+1FiFiFi1]
=
[Fi1Fi2Fi2Fi3]
[1110]
^2
[Fi+1FiFiFi1]=[Fi1Fi2Fi2Fi3][1110]2

以此类推,继续把矩阵拆开,最终右边就只剩初始矩阵的 i i i次方
[ F i + 1 F i F i F i − 1 ] = [ 1 1 1 0 ] i
[Fi+1FiFiFi1]
=
[1110]
^i
[Fi+1FiFiFi1]=[1110]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];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

学完一定要及时练习一下这些方法:斐波那契数列练习

矩阵乘法

两个矩阵相乘如果用朴素算法(就是按乘法规则一个一个元素的乘起来)的话时间复杂度是 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 ]

[c11c12c1nc21c22c2ncn1cn2cnn]
=
[a11a12a1na21a22a2nan1an2ann]
\cdot
[b11b12b1nb21b22b2nbn1bn2bnn]
c11c21cn1c12c22cn2c1nc2ncnn = a11a21an1a12a22an2a1na2nann b11b21bn1b12b22bn2b1nb2nbnn
↓ \downarrow
[ r s t u ] = [ a b c d ] ⋅ [ e f g h ]
[rstu]
=
[abcd]
\cdot
[efgh]
[rtsu]=[acbd][egfh]

其中
{ r = a e + b g s = a f + b h t = c e + d g u = c f + d h \left\{
r=ae+bgs=af+bht=ce+dgu=cf+dh
\right.
r=ae+bgs=af+bht=ce+dgu=cf+dh

但是这样子真的优化了矩阵乘法嘛?我们可以分析一下这个算法的时间复杂度,因为有 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),啊?优化了个寂寞。虽然没有优化,但是这个递归式给了我们优化的思路,我们可以想办法降低递归的时间消耗,就是减少计算矩阵乘法的次数或是改变分块的方式,改变分块的方式显然是行不通的,因为你将一个块矩阵分的小一些,其他块矩阵一定会变大,相当于没怎么变,那就只有减少计算矩阵乘法的次数了。

下面给出的算法非常神奇,一般人真的想不到

Strassen

算法主要想法就是减少矩阵乘法,为此给出了七个式子
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 )

P1=a(fh)P2=(c+b)hP3=(c+d)eP4=d(g3)P5=(a+d)(e+h)P6=(bd)(g+h)P7=(ac)(e+f)
P1P2P3P4P5P6P7=a(fh)=(c+b)h=(c+d)e=d(g3)=(a+d)(e+h)=(bd)(g+h)=(ac)(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
r=P5+P4P2+P6s=P1+P2t=P3+P4u=P5+P1P3P7
rstu=P5+P4P2+P6=P1+P2=P3+P4=P5+P1P3P7

这里我就不证明了,如果你有兴趣可以推导试试看,通过这样的构造,每层递归就只需要计算 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)

分治练习

合并 K 个升序链表
跳石头
Secret Cow Code S

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/716322
推荐阅读
相关标签
  

闽ICP备14008679号