分治策略
递归式
递归式的形式
一个递归式就是一个等式或不等式,它通过更小的输入上的函数值来描述一个函数。
求解递归式的方法
-
代入法 我们猜测一个界,然后用数学归纳法证明这个界是正确的。
-
递归树法 将递归式转化为一棵树,其结点表示不同层次的递归调用产生的代价,然后采用边界和技术来求解递归式。
-
主方法 可求解形如下面公式的递归式的界:
这种形式的递归式很常见,它刻画了这样一个分治算法:生成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]所处的位置必然是以下三种情况之一:
- 完全位于子数组A[low , mid]中,因此low ≤ i ≤ j ≤ mid。
- 完全位于子数组A[mid +1,high]中,因此mid <i ≤ j ≤ high。
- 跨越了中点,因此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的运行时间为:
用代入法求解递归式
代入法求解递归式分为两步:
- 猜测解的形式
- 用数学归纳法求出解中的常数,并证明解是正确的。
我们可以用代入法为递归式建立上界或下界。
做好的猜测:
- 一般来说,增加常数项不会显著影响递归式的解,如当n较大时,n/2和n/2+18差距不大,可以猜测为相似的解。
- 可以先证明递归式轻松的上界和下界,然后缩小不确定的范围。
用递归树的方法求解递归式
递归树最适合生成好的猜想,然后即可用代入法来验证猜测是否正确。
用主方法求解递归式
主方法主要针对如下形式的递归式:
它将规模为n的问题分解为a个子问题,每个子问题规模为n/b,递归求解,每个子问题花费时间为T(n/b)。f(n)包含了问题分解和子问题合并的代价。