许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或者多次的递归调用其自身以解决紧密相关的若干子问题。这些算法典型的遵循分治法是思想。
分治法的思想
将原问题分解为几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治模式在每层递归时都有三个步骤:
分解原问题为若干子问题:这些子问题都是原问题规模较小的实例。
解决这些子问题,递归的求解各个子问题,若子问题的规模足够小,则直接求解。
合并这些子问题的解为原问题的解。
分治法适用条件
1、问题的规模缩小到一定的程度就可以容易地解决; 2、问题可以分解为若干个规模较小的相同问题; 3、利用该问题分解出的子问题的解可以合并为该问题的解;
4、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
其中,第2条是分治策略适用的前提条件,也是递归的基本条件;第三条是适用分治策略的关键,能否使用分治策略,完全取决于第三条。
典型例子
例子1:归并排序
归并排序的思想:将数组拆分为左右两个部分,然后分别保证左右两边有序,最后将左右两边进行合并,使得整个数组有序。
归并排序最大的难点在于合并。在合并的过程中,我们可以假想一下将左右两边有序的数组看成是两堆整理有序的正面朝上的扑克牌,现在的任务就变成了将两堆有序的扑克牌合并为一堆。解决该问题最简单的办法是找一个空地方形成第三个堆,每次都对两堆牌的最上面一张进行比较,从中选择一张最小的(或最大的)放在第三个堆上,当两堆中的任何一堆没牌了,然后把另外一堆剩下的牌放到第三堆上即可。
public class MergeSort { public static void main(String[] args) { int[] A = { 0, 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7 }; divideAndConquer(A, 0, A.length - 1); for (int i = 0; i < A.length; i++) { System.out.print(A[i] + " "); } } public static void divideAndConquer(int[] A, int left, int right) { if (left == right) { return; } int media = (left + right) / 2; divideAndConquer(A, left, media); divideAndConquer(A, media + 1, right); merge(A, left, media, right); } //合并 public static void merge(int[] A, int left, int media, int right) { int[] temp = new int[right - left + 1]; int p = left; int q = media+1; int i = 0; while (p <= media && q <= right) { if (A[p] <= A[q]) { temp[i] = A[p]; p++; i++; } else { temp[i] = A[q]; q++; i++; } } for (; p <= media; p++,i++) { temp[i] = A[p]; } for (; q <= right; q++,i++) { temp[i] = A[q]; } i = 0; for (int j = left; j <= right; j++, i++) { A[j] = temp[i]; } } }例子2 最大子数组问题
原始问题描述:假设你打算从某一个上市公司购买股票,买卖规则是当你买进股票后,你可以选择在之后的任意一天把股票卖出,买进卖出都是在当天交易结束后进行,为了补偿这一限制,你可以知道这支股票将来的价格,你的目标是最大化收益。假设未来一段时间内的股票价格如下:
{ 100, 113, 110, 85, 105, 102, 86, 63, 81, 101, 94, 106, 101, 79, 94, 90, 97 }
解决方案1:暴力解决:枚举所有可能买进卖出的情况,求收益的最大值
public static String BruteForce(int[] A) { int len = A.length; int[][] profit = new int[len][len]; int buy = -1; int sold = -1; int maxProfit = -9999; for (int i = 0; i < len; i++) { for (int j = i; j < len; j++) { profit[i][j] = A[j] - A[i]; if (maxProfit < profit[i][j]) { maxProfit = profit[i][j]; buy = i; sold = j; } } } return "buy:" + buy + ",sold:" + sold + ",profit:" + maxProfit; }解决方案2:分治策略
换个角度来看,按第一天为标准,将股价转化为股价的波动情况如下所示:
A=[0,13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7], 那么所求的最大收益也就是求连续时间段内股价波动的和的最大值。转化为最大子数组问题。
解决思路:首先将数组平均拆分为左右两个部分(left<=media<right),那么最大收益一定是左边部分收益,右边部分收益和中间部分收益(从left至right求和)的最大值。首先,先求得左右两边的最大收益:maxLeft,maxRight;然后求得中间部分的收益maxMedia,最后返回三个数的最大值即可。
public static int devideAndConquer(int[] A, int left, int right) { int media = (left + right) / 2; int maxLeft = 0; int maxRight = 0; if (left == right) { return A[left]; } // 左边最大子数组和 maxLeft = devideAndConquer(A, left, media); // 右边最大子数组和 maxRight = devideAndConquer(A, media + 1, right); // 跨界最大子数组和 int maxSumLeft = 0; int maxSumRight = 0; int sumLeft = 0; int sumRight = 0; for (int i = media; i >= left; i--) { sumLeft += A[i]; if (maxSumLeft < sumLeft) { maxSumLeft = sumLeft; } } for (int j = media + 1; j <= right; j++) { sumRight += A[j]; if (maxSumRight < sumRight) { maxSumRight = sumRight; } } return Math.max(Math.max(maxLeft, maxRight), maxSumLeft + maxSumRight); }
