《算法导论3rd第四章》分治策略

前言

前面我们介绍了归并排序,它利用了分治策略。分治策略一般步骤:

  1. 分解,即将一个问题划分为一些子问题
  2. 解决,当子问题规模足够小,则停止递归,直接求解
  3. 合并,将子问题的解合成原问题的解

最大子数组问题

类似于letcode中的《53. 最大子序和

求连续子数组的最大和
题目描述:
输入一个整形数组,数组里有正数也有负数。
数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
求所有子数组的和的最大值。要求时间复杂度为O(n)。

例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,
因此输出为该子数组的和18。

暴力法

即2个for循环,将所有结果都算上一遍,比较大小。

/************************************************************************/
/*  暴力法
/************************************************************************/
void MaxSubArraySum_Force(int arr[], vector<int> &subarr, int len)
{
    if (len == 0)
        return;
    int nMax = INT_MIN;
    int low = 0, high = 0;
    for (int i = 0; i < len; i ++) {
        int nSum = 0;
        for (int j = i; j < len; j ++) {
            nSum += arr[j];
            if (nSum > nMax) {
                nMax = nSum;
                low = i;
                high = j;
            }
        }
    }
    for (int i = low; i <= high; i ++) {
        subarr.push_back(arr[i]);
    }
}

这种方式的时间复杂度是O(n^2),不符合题目O(n)的要求。

分治法

假设数组为A[low...high],利用分治策略

  1.  分解,将数组A拆分为A[low...mid]和A[mid...high] .
  2. 解决,当数组A长度为1时,直接返回解
  3. 合并,将解进行合并,主要三种情况出现
    1. 最大子数组为于A[low...mid]中(左边)
    2. 最大子数组为于A[mid...high]中(右边)
    3. 最大子数组包含mid(中间)
/************************************************************************/
/*    分治法
    最大和子数组有三种情况:
    1)A[1...mid]
    2)A[mid+1...N]
    3)A[i..mid..j]
/************************************************************************/
//肯定包含mid,即从mid出发向两边
int Find_Max_Crossing_Subarray(int arr[], int low, int mid, int high)
{
    const int infinite = -9999;
    int left_sum = infinite;
    int right_sum = infinite;

    int max_left = -1, max_right = -1;

    int sum = 0; //from mid to left;
    for (int i = mid; i >= low; i --) {
        sum += arr[i];
        if (sum > left_sum) {
            left_sum = sum;
            max_left = i;
        }
    }
    sum = 0;  //from mid to right
    for (int j = mid + 1; j <= high; j ++) {
        sum += arr[j];
        if (sum > right_sum) {
            right_sum = sum;
            max_right = j;
        }
    }
    return (left_sum + right_sum);
}

int Find_Maximum_Subarray(int arr[], int low, int high)
{
    if (high == low) //only one element;
        return arr[low];
    else {
        int mid = (low + high)/2;
        int leftSum = Find_Maximum_Subarray(arr, low, mid);
        int rightSum = Find_Maximum_Subarray(arr, mid+1, high);
        int crossSum = Find_Max_Crossing_Subarray(arr, low, mid, high);

        if (leftSum >= rightSum && leftSum >= crossSum)
            return leftSum;
        else if (rightSum >= leftSum && rightSum >= crossSum)
            return rightSum;
        else
            return crossSum;
    }
}

这种方式的时间复杂度是O(nlgn),不符合题目O(n)的要求。

区间法

习题4.1-5,你又有了另外一种思路:数组A[1...j+1]的最大和子数组,有两种情况:

  • a) A[1...j]的最大和子数组; 
  • b) 某个A[i...j+1]的最大和子数组,即A[1...i] < 0
/************************************************************************/
/*    区间法
    求A[1...j+1]的最大和子数组,有两种情况:
    1)A[1...j]的最大和子数组
    2)某个A[i...j+1]的最大和子数组
/************************************************************************/
void MaxSubArraySum_Greedy(int arr[], vector<int> &subarr, int len)
{
    if (len == 0)
        return;
    int nMax = INT_MIN;
    int low = 0, high = 0;
    int cur = 0; //一个指针更新子数组的左区间
    int nSum = 0;
    for (int i = 0; i < len; i ++) {
        nSum += arr[i];
        if (nSum > nMax) {
            nMax = nSum;
            low = cur;
            high = i;
        }
        if (nSum < 0) {
            cur = i + 1;
            nSum = 0;
        }
    }
    for (int i = low; i <= high; i ++)
        subarr.push_back(arr[i]);
}

这种方式的时间复杂度是O(n),符合题目要求。

动态规划

动态规划算法最主要的是寻找递推关系式(前面状态和后面状态的关系), 其递推式为

sum[i+1] = Max(sum[i] + A[i+1], A[i+1])

/************************************************************************/
/*  动态规划(对应着上面的贪心法看,略有不同)
    求A[1...j+1]的最大和子数组,有两种情况:
        1)A[1...j]+A[j+1]的最大和子数组
        2)A[j+1]
    dp递推式:
        sum[j+1] = max(sum[j] + A[j+1], A[j+1])
/************************************************************************/
int MaxSubArraySum_dp(int arr[], int len)
{
    if (len <= 0)
        exit(-1);
    int nMax = INT_MIN;
    int sum = 0;

    for (int i = 0; i < len; i ++) {
        if (sum >= 0)
            sum += arr[i];
        else
            sum = arr[i];
        if (sum > nMax)
            nMax = sum;
    }
    return nMax;
}

这种方式的时间复杂度也是O(n),相对于区间法,代码更加简洁。

练习

  1. 1-1 当A的所有元素均为负数时,FIND-MAXIMUM-SUBARRAY 返回什么
    A中最大的负数
  2. 1-2 对最大子数组问题,编写暴力求解方法的伪代码, 其运行时间应该为Θ(n²)
    略,已在文章讲解
  3. 1-3 在你的计算机上实现最大子数组问题的暴力算法和递归算法。请指出多大的问题规模n0是性能交叉点——从此之后递归算法将击败暴力算法?然后,修改递归算法的基本情况——当问题规模小于n0时采用暴力算法。修改后,性能交叉点会改变吗?

    n0=47的时候,从暴力切换到递归
  4. 1-4 假定修改最大子数组问题的定义,允许结果为空子数组,其和为0。你应该知道如何修改现有算法,使它们能允许空子数组为最终结果?

    当结果为负数的时候,返回0

矩阵乘法的Strassen算法

假设 A 为m \times p 的矩阵,B为 p \times n 的矩阵,那么称 m \times n的矩阵 C为矩阵 A与 B的乘积,记作 C=AB ,称为矩阵积. 

其中矩阵 C 中的第 i行第j列元素可以表示为:

假如在矩阵 A 和矩阵 B中, m=n=p=N ,那么完成 C=AB 需要多少次乘法呢?

  1. 对于每一个行  ,总共有 N 行;
  2. 对于每一个列  ,总共有 N列;
  3. 计算它们的内积,总共有 N 次乘法计算。

综合可以看出,矩阵乘法的算法复杂度是:O(N^3) 。

再把矩阵相乘的思维提升到方阵相乘

假设矩阵 A 和矩阵 B 都是 N * N(N=2^n) 的方矩阵,求 C=AB,如下所示:

A = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} B = \begin{bmatrix} B_{11} & B_{12} \\ B_{21} &B_{22} \end{bmatrix} C = \begin{bmatrix} C_{11} & C_{12} \\ C_{21} &C_{22} \end{bmatrix}

其中 

\begin{bmatrix} C_{11} & C_{12} \\ C_{21} &C_{22} \end{bmatrix} = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} * \begin{bmatrix} B_{11} & B_{12} \\ B_{21} &B_{22} \end{bmatrix}

矩阵 C 可以通过下列公式求出:

\\ C_{11}= A_{11}B_{11} + A_{12}B_{21} \\ C_{12}= A_{11}B_{12} + A_{22}B_{21} \\ C_{21}= A_{21}B_{11} + A_{22}B_{21} \\ C_{22}= A_{21}B_{12} + A_{22}B_{22}

从上述公式我们可以得出,计算2个 n * n 的矩阵相乘需要2个 n/2 * n/2 的矩阵8次乘法和4次加法。我们使用 T(n) 表示 n * n 矩阵乘法的时间复杂度,那么我们可以根据上面的分解得到下面的递推公式:

Strassen原理详解

那么有没有比 O(N^3)  更快的算法呢?从上述递归工式可以看出每次递归操作都需要8次矩阵相乘,而这正是瓶颈的来源。相比加法,矩阵乘法是非常慢的,于是我们想到减少矩阵相乘的次数使用加法替换。Strassen算法正是从这个角度出发,实现了降低算法复杂度!实现步骤可以分为以下4步:

  1. 按上述方法将矩阵 A,B,C 分解(花费时间 O(1))
  2. 如下创建10个 n/2 * n/2 的矩阵S1,S2....S10  (花费时间O(n^2))

  3. 递归地计算7个矩阵积 P1,P2,P3....P7 ,每个矩阵 P都是 n/2 * n/2的

  4. 通过 P 计算 C11,C12,C21,C22 ,花费时间 O(n^2)

综合可得如下递归式:

练习

  1. 2-1使用Strassen算法计算\begin{bmatrix} 1 & 3 \\ 7 & 5 \end{bmatrix} \begin{bmatrix} 6 &8\\ 4&2\end{bmatrix},给出过程
    1. 拆解矩阵
      A11=(1),A12=(2),A21=(7),A22=(5),B11=(6),B12=(8),B21=(4),B22=(2)
    
    2. 创建10矩阵
    S1=B12−B22=6
    S2=A11+A12=4
    S3=A21+A22=12
    S4=B21−B11=−2
    S5=A11+A22=6
    S6=B11+B22=8
    S7=A12−A22=−2
    S8=B21+B22=6
    S9=A11+A21=−6
    S10=B11+B12=14
    
    3. 递归计算7矩阵
    P1=A11⋅S1=1⋅6=6
    P2=S2⋅B22=4⋅2=8
    P3=S3⋅B11=6⋅12=72
    P4=A22⋅S4=6⋅12=72
    P5=S5⋅S6=6⋅8=48
    P6=S7⋅S8=(−2)⋅6=−12
    P7=S9⋅S10=(−6)⋅14=−84
    
    4. 得出结果
    C11=P5+P4−P2+P6=48+(−10)−8+(−12)=18
    C12=P1+P2=6+8=14
    C21=P3+P4=72+(−10)=62
    C22=P5+P1−P3−P7=48+6−72−(−84)=66
  2. 2-2写出Strassen算法伪代码
    STRASSEN(A, B)
        n = A.rows
        if n == 1
            return a[1, 1] * b[1, 1]
        let C be a new n × n matrix
        A[1, 1] = A[1..n / 2][1..n / 2]
        A[1, 2] = A[1..n / 2][n / 2 + 1..n]
        A[2, 1] = A[n / 2 + 1..n][1..n / 2]
        A[2, 2] = A[n / 2 + 1..n][n / 2 + 1..n]
        B[1, 1] = B[1..n / 2][1..n / 2]
        B[1, 2] = B[1..n / 2][n / 2 + 1..n]
        B[2, 1] = B[n / 2 + 1..n][1..n / 2]
        B[2, 2] = B[n / 2 + 1..n][n / 2 + 1..n]
        S[1] = B[1, 2] - B[2, 2]
        S[2] = A[1, 1] + A[1, 2]
        S[3] = A[2, 1] + A[2, 2]
        S[4] = B[2, 1] - B[1, 1]
        S[5] = A[1, 1] + A[2, 2]
        S[6] = B[1, 1] + B[2, 2]
        S[7] = A[1, 2] - A[2, 2]
        S[8] = B[2, 1] + B[2, 2]
        S[9] = A[1, 1] - A[2, 1]
        S[10] = B[1, 1] + B[1, 2]
        P[1] = STRASSEN(A[1, 1], S[1])
        P[2] = STRASSEN(S[2], B[2, 2])
        P[3] = STRASSEN(S[3], B[1, 1])
        P[4] = STRASSEN(A[2, 2], S[4])
        P[5] = STRASSEN(S[5], S[6])
        P[6] = STRASSEN(S[7], S[8])
        P[7] = STRASSEN(S[9], S[10])
        C[1..n / 2][1..n / 2] = P[5] + P[4] - P[2] + P[6]
        C[1..n / 2][n / 2 + 1..n] = P[1] + P[2]
        C[n / 2 + 1..n][1..n / 2] = P[3] + P[4]
        C[n / 2 + 1..n][n / 2 + 1..n] = P[5] + P[1] - P[3] - P[7]
        return C
  3. 2-3如何修改Strassen算法,适应矩阵规模不是2的幂的情况,并证明算法运行时间O(n^lg7)
    可以将矩阵填充为 n×n 的矩阵,在 Θ(nlg7) 内求解完成后,再将填充元素剥离掉。
  4. 2-4 如果可以用k次乘法操作(假定乘法的交换律不成立)完成两个3 × 3矩阵相乘,那么你可以在o(n^{​{\rm lg}7})时间内完成n × n矩阵相乘,满足这一条件的最大k是多少?此算法的运行时间是怎样的?
  5. 2-5 V.Pan发现一种方法,可以用132464次乘法操作完成68 × 68的矩阵相乘,发现另一种方法,可以用143640次乘法操作完成70 × 70的矩阵相乘,还发现一种方法,可以用155424 次乘法操作完成72 × 72 的矩阵相乘。当用于矩阵相乘的分治算法时,上述哪种方法会得到最佳的渐近运行时间?与Strassen算法相比,性能如何?
    对于采用分治法的矩阵乘法算法来说,其运行时间都为Θ(n^d)
    
    log_68{132464}≈2.795128
    log_70{143640}≈2.795122
    log_72{155424}≈2.795147
    
    70×70 的矩阵乘法最快,lg7≈2.81 ,比Strassen算法好。
  6. 2-6用Strassen算法作为子过程来进行一个kn×n矩阵和一个n×kn矩阵相乘,最快需要花费多长时间?对两个输入矩阵规模互换的情况,回答相同的问题。
  7. 2-7 设计算法,仅使用三次实数乘法即可完成复数a + bi 和 c+di相乘。算法需接收a 、 b 、 c和d 为输入,分别生成实部ac−bd和虚部ad+bc。
    借鉴Strassen算法的思想,该问题可以按以下步骤解决。
     1.计算P1,P2和P3
        P1 = ad 
        P2 = bc
        P3 = (a–b)(c+d) =ac–bd+ad–bc
     2.计算实部和虚部
        实部:P3 – P1 + P2 = ac − bd 
        虚部:P1 + P2 = ad + bc
    
      该算法只需要3次乘法即可。
    

用代入法求解递归式

代入法求解递归式分为两步:

  1. 猜测解的形式。
  2. 用数学归纳法求出解中的常数,并且证明解是正确的。

这种方法很强大,但是我们必须能够猜出解的形式,以便将其代入。例如我们确定下面递归式的上界:


T(n)=2T(\lfloor n/2\rfloor)+n \qquad (4.19)

我们猜测其解为T ( n ) = O ( n l g n ),即存在c>0 符合 T(n)\leq cnlgn
我们将其代入到递归式,只要c ≥ = 1,以下公式就会成功成立。
 

\\ T(n)\leq2(cn/2lg(n/2))+n = cnlg(n/2)+n \\ =cnlgn - cnlg2+n = cnlgn - cn+n\leq cnlgn
 

练习

  1. 3-1 证明T(n)=T(n-1)+n的解为O(n^2).

    根据即证明 存在c使得T(n)<=cn^2成立
    
    即 T(n)≤c(n−1)^2+n=cn^2−2cn+c+n
    
    当 c=1; 
    
    n^2−2n+1+n=n2−n+1≤n2  for n≥1
  2. 3-2 证明: T(n) = T( \lceil n/2\rceil) + 1的解为O(lgn)

    证明:T(n)≤clg⁡n
    我猜测: T(n)≤clg(n-2)
    
    即
    T(n)≤clg(⌈n/2⌉−2)+1≤clg(n/2+1−2)+1≤clg((n−2)/2)+1≤clg(n−2)−clg2+1
    
    只要 c≥1 
    T(n)≤clg(n−2)≤clg⁡n
  3. 3-3 我们看到T(n)=2T(\lfloor n/2 \rfloor) + n的解为 O(nlgn). 证明: Ω(nlgn)也是这个递归式的解。从而得出结论:解为Θ(nlgn)

  4. 3-4 通过做出不同的归纳假设,我们不必调整归纳证明中的边界条件,即可客服递归式(4.19)中边界条件T(1)=1带来的困难。 

  5. 3-5 证明:归并排序的严格递归式T(n)=T(\lfloor n/2\rfloor)+ T(\lceil n/2\rceil)+n的解为Θ(nlgn)

  6. 3-6 证明: T(n)=2T(\lfloor{n/2}\rfloor+17)+n的解为Θ(nlgn)

  7. 3-7使用4.5节中的主方法,可以证明 T(n)=4T(n/3)+n的解为T(n)=\Theta({n^{\log{_3} {4}}}). 说明基于假设T(n) \le{c{n^{\log{_3} {4}}}}的代入法不能证明这一结论。然后说明如何通过减去一个低阶项完成代入法证明

  8. 3-8 使用4.5节中的主方法,可以证明T(n)=4T(n/2)+n的解为T(n) = \Theta({n^2}). 说明基于假设T(n)\leq{cn^{2}} 的代入法不能证明这一结论。然后说明如何通过减去一个低阶项完成代入法证明

  9. 3-9 利用改变变量的方法求解递归式T(n)=3T(\sqrt{n}) + \log{n}. 你的解应该是渐进紧确的。不必担心数值是否是整数

递归树法求解递归式

代换法有时很难得到一个正确的好的猜测值。递归树最适合用来生成好的猜测,然后即可以用代入法去验证猜测是否正确。 在递归树当中,每个结点表示一个单一子问题的代价,子问题对应某次递归函数的调用。我们将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价。以递归式T(n)=3T(\lfloor n/4 \rfloor )+cn^2为例。我们构造出递归树,如下图所示


我们求递归树中所有层次的代价之和,则可确定整棵树的代价

这样,对于原始的递归式,我们就推导出了一个猜测T(n)=O(n^2)。我们使用代入的方法去验证猜测是正确的,即T(n)=O(n^2)是递归式的一个上界。

 练习

  1. 4-1 对递归式T(n)=3T(\lfloor{n/2}\rfloor)+n,利用递归树确定一个好的渐进上界,用代入法进行验证。
  2.  4-2 对递归式T(n)=T(n/2)+n^2,利用递归树确定一个好的渐进上界,用代入法进行验证。
  3. 4-3 对递归式T(n)=4T(n/2+2)+n,利用递归树确定一个好的渐进上界,用代入法进行验证。
  4. 4-4 对递归式T(n)=T(n-1)+1,利用递归树确定一个好的渐进上界,用代入法进行验证。
  5. 4-5 对递归式T(n)=T(n-1)+T(n/2)+n,利用递归树确定一个好的渐进上界,用代入法进行验证。
  6. 4-6 对递归式T(n)=T(n/3)+T(2n/3)+cn利用递归树论证其解为Ω(nlgn),其中c为常数。
  7. 4-7 对递归式T(n)=4T(\lfloor{n/2}\rfloor)+cn(c为常数),画出递归树,并给出其解的一个渐进紧确界。用代入法进行验证。
  8. 4-8 对递归式T(n)=T(n-a) + T(a) + cn,利用递归树给出一个渐进紧确解,其中a≥1和c>0是常数。

  9. 4-9 对递归式T(n) = T(\alpha{n})+T((1-\alpha)n)+cn,利用递归树给出一个渐进紧确解,其中0<α<1和c>0常数。(略)

 主方法求解递归式

 主方法主要针对形如T(n) = af(n/b) + f(n)的递归式,它可以瞬间估计一个递推式的算法复杂度。书本上以”菜谱“来描述这种方法的好用之处

粗略的总结:主要看 f(n) 和 nlog_ba 的关系,谁大取谁,相等则两个相乘,但要注意看是否相差因子 n^\epsilon。对于3),还要看是否满足条件 af(n/b) <= cf(n) . 就像上面所说的,该方法不能用于所有的形如上式的递归式

练习

  1. 5-1 对下列递归式, 使用主方法求出渐近紧确界。
  2. 5-2 Caesar教授想设计一个渐近快于Strassen算法的矩阵相乘算法。他的算法使用分治方法,将每个矩阵分解为{n/4}\times{n/4}的子矩阵,分解和合并共花费\Theta({n^2})时间。他需要确定,他的算法需要创建多少个子问题,才能击败Strassen算法。如果他的算法创建a个子问题,则描述运行时间T(n)的递归式为T(n)=aT(n/4)+\Theta({n^2})。Caesar教授的算法如要要渐进快于Strassen算法,a的最大整数值应是多少?
    Strassen递归式:T(n)=7T(n/2)+Θ(n2)=Θ(nlg7)
    情况三可以排除(矩阵乘法 T(n)>Θ(n^2))
    
    log_4{⁡a}= lga/lg4= lga/2 <lg⁡7,
    => lga<lg49
    
    所以 a 最大值为48。
    
  3. 5-3 使用主方法证明: 二分查找递归式T(n)=T(n/2)+\Theta(1)的解是T(n)=\Theta(\lg{n})
  4. 5-4 主方法能应用于递归式T(n)=4T(n/2)+n^{2}\lg{n}吗?请说明为什么可以或者为什么不可以。给出这个递归式的一个渐近上界。
  5. 5-5 考虑主定理情况3的一部分:对某个常数c<1, 正则条件af(n/b)\le cf(n)是否成立。给出一个例子,其中常数a≥1,b>1且函数(n)满足主定理的情况3中除正则条件外的所有条件。

主要参考

算法导论第四章分治策略实例解析(一)

算法导论第四章分治策略实例解析(二)

详解矩阵乘法中的Strassen算法

算法导论第三版 第4章习题答案

算法导论 — 4.2 矩阵乘法的Strassen算法

猜你喜欢

转载自blog.csdn.net/y3over/article/details/9414627
今日推荐