算法导论真的好(厚)啊,才看到分治策略

661 阅读5分钟

分治策略

递归式

递归式的形式

一个递归式就是一个等式或不等式,它通过更小的输入上的函数值来描述一个函数。

求解递归式的方法

  • 代入法 我们猜测一个界,然后用数学归纳法证明这个界是正确的。

  • 递归树法 将递归式转化为一棵树,其结点表示不同层次的递归调用产生的代价,然后采用边界和技术来求解递归式。

  • 主方法 可求解形如下面公式的递归式的界:

    T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)
    其中a1b1,f(n)是一个给定的函数其中a≥1,b>1,f(n)是一个给定的函数

    这种形式的递归式很常见,它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为f(n).

递归式技术细节

当声明、求解递归式时,我们常常忽略向下取整、向上取整及边界条件。

最大子数组问题

股票最大收益问题

暴力求解方法

简单的尝试每对可能的买进和卖出日期组合,只要卖出日期在买进日期之后即可。

对于数组的每一个数,都计算其后所有的数与其差值,找出最大的差值。时间复杂度是O(n²)。

public static int[] findMaximumSubarray(int[] A) {
    int maxSum = Integer.MIN_VALUE,sum = 0;
    int left = -1,right = -1;
    for (int i = 0; i < A.length; i++) {
        sum = A[i];
        for (int j = i + 1; j < A.length; j++) {
            sum += A[j];
            if (sum > maxSum) {
                maxSum = sum;
                left = i;
                right = j;
            }
        }
    }
    return new int[] {left,right,maxSum};
}
问题变换

不再从每日价格的角度去看待输入数据,而是考察每日价格变化,第i天的价格变化定义为第 i 天和第 i - 1 天的价格差。如果讲真这一行数据看做是数组A,那么问题就转化为寻找A的和最大的非空连续子数组,称为连续数组的最大子数组。

只有数组中包含负数时,最大子数组问题才有意义。如果所有数组元素都是非负数,最大子数组即为整个数组,整个数组的和肯定是最大的。

分治策略求解方法

假定要寻找子数组A[low , high]的最大子数组,首先找到子数组的中间位置mid,然后考虑求解两个子数组A[low , mid]和A[mid + 1, high]。

A[low , high]的任何连续子数组A[i , j]所处的位置必然是以下三种情况之一:

  1. 完全位于子数组A[low , mid]中,因此low ≤ i ≤ j ≤ mid。
  2. 完全位于子数组A[mid +1,high]中,因此mid <i ≤ j ≤ high。
  3. 跨越了中点,因此low ≤ i ≤ mid < j ≤ high。

A[low , high]的最大子数组一定是以上三种情况的所有子数组中和最大者。可以递归的求解A[low , mid]和A[mid +1,high]的最大子数组,剩余的全部工作就是寻找跨越中点的最大子数组,然后选取和最大者。

  • 求跨越中点的最大子数组

    任何跨越中点的子数组都由两个子数组A[i , mid]和A[mid + 1 , j]组成,low ≤ i ≤ mid < j ≤ high。故只需找出形如A[i , mid]和A[mid + 1 , j]的最大子数组,然后将二者合并即可。时间复杂度为O(n)。

    用java实现的代码如下:

    public static int[] findMaxCrossingSubarray(int[] A,int low,int mid,int high) {
            //求出左半部A[low,mid]的最大子数组[i,mid]
            int leftSum = Integer.MIN_VALUE;
            int sum = 0;
            int maxLeft = 0;
            for (int i = mid; i >= low; i--) {
                sum += A[i];
                if (sum > leftSum) {
                    leftSum = sum;
                    maxLeft = i;
                }
            }
            //求出右半部A[mid+1,high]的最大子数组[mid+1,j]
            int rightSum = Integer.MIN_VALUE;
            sum = 0;
            int maxRight = 0;
            for (int i = mid + 1; i <= high; i++) {
                sum += A[i];
                if (sum > rightSum) {
                    rightSum = sum;
                    maxRight = i;
                }
            }
        	//返回合并后的最大子数组左下标maxLeft和右下标maxRight,以及最大子数组的和。
            return new int[] {maxLeft,maxRight,leftSum + rightSum};
        }
    
  • 求整个数组的最大子数组

    代码如下:

    public static int[] findMaximumSubarray(int[] A,int low,int high) {
            if (low == high) {
                return new int[] {low,high,A[low]};
            } else {
                int mid = (low + high) / 2;
                int[] leftMaximumSubarray = findMaximumSubarray(A,low,mid);//求左子数组的最大子数组
                int[] rightMaximumSubarray = findMaximumSubarray(A,mid + 1,high);//求右子数组的最大子数组
                int[] maxCrossingSubarray = findMaxCrossingSubarray(A,low,mid,high);//求跨越中点的子数组的最大子数组
                if (leftMaximumSubarray[2] >= rightMaximumSubarray[2] && leftMaximumSubarray[2] >= maxCrossingSubarray[2]) {
                    return leftMaximumSubarray;
                } else if (rightMaximumSubarray[2] >= leftMaximumSubarray[2] && rightMaximumSubarray[2] >= maxCrossingSubarray[2]) {
                    return rightMaximumSubarray;
                } else {
                    return maxCrossingSubarray;
                }
            }
            
        }
    

注意:

  • 当A的所有元素为负数时,findMaximumSubarray返回数组中最大的负数。

更好的方法——动态规划

public static int[] findMaximumSubarray(int[] A) {
    int[] dp = new int[A.length]; //dp[i]表示A[0..i]中以A[i]结尾的序列的最大子序列和,A[n - 1]并不是整个数组的最大子序列和
    int maxSum = A[0];
    dp[0] = A[0];
    int left = 0,right = 0;
    for (int i = 1; i < A.length; i++) {
        if (A[i] >= A[i] + dp[i - 1]) {
            dp[i] = A[i];
            left = i;
        } else {
            dp[i] = dp[i - 1] + A[i];
            if (dp[i] > maxSum) {
                maxSum = dp[i];
                right = i;
            }
        }
    }
    return new int[] {left,right,maxSum};
}

时间复杂度为O(n),空间复杂度为O(n)。

分治算法的分析

findMaximumSubarray的运行时间为:

T(n)=O(1),n=1T(n)=2T(n/2)Θ(n),n>1T(n) = O(1),若n = 1; T(n) = 2T(n / 2) + Θ(n),若n > 1

用代入法求解递归式

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

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

我们可以用代入法为递归式建立上界或下界。

做好的猜测:

  1. 一般来说,增加常数项不会显著影响递归式的解,如当n较大时,n/2和n/2+18差距不大,可以猜测为相似的解。
  2. 可以先证明递归式轻松的上界和下界,然后缩小不确定的范围。

用递归树的方法求解递归式

递归树最适合生成好的猜想,然后即可用代入法来验证猜测是否正确。

用主方法求解递归式

主方法主要针对如下形式的递归式:

T(n)=aT(n/b)f(n),其中a1b1是常数,f(n)是渐近正函数。T(n) = aT(n/b) +f(n),其中a≥1,b>1是常数,f(n)是渐近正函数。

它将规模为n的问题分解为a个子问题,每个子问题规模为n/b,递归求解,每个子问题花费时间为T(n/b)。f(n)包含了问题分解和子问题合并的代价。