分治策略——算法导论(3)

1. 从一个股价的问题说起

    假如你获得了一种可以预测未来某公司股价的能力。下图是你预测的股价情况,那么你会在哪一天买入,哪一天卖出呢?

 

    你可能认为可以在这17天当中的股价最低的那天(第7天)买入,然后在之后的股价最高的那天(第11天)卖出;或者反过来在整段时间内股价最高的那天卖出,然后在之前的股价最低的那天买入。如果这种策略是可行的,那么确定最大收益将非常简单。但这种策略不是经常奏效的。比如下图的这种情况:

很容易看出在第2天买入,第3天卖出,将获得最大的收益。但第二天并非最低(事实上股价最低是第4天),第3天也并非股价最高(股价最高是第1天)。

 

2. 暴力方法(穷举法 Exhaustive method)求解

    我们最容易想到的方法应该是暴力求解方法(事实上在条件允许的时候,暴力破解方法不失为是一种好方法)。分析该方法可发现,n天中我们需要尝试的组合将有 种,而处理每对日期的时间至少是常量时间,因此,这种方法的运行时间是θ(n2)。下面给出这种算法的Java实现代码:

public static void main(String[] args) {
	int[] priceArray = new int[] { //
			100, 113, 110, 85, //
			105, 102, 86, 63, //
			81, 101, 94, 106, //
			101, 79, 94, 90, //
			97 };
	int maxDifference = Integer.MIN_VALUE;
	int startDay = -1;
	int endDay = -1;
	for (int i = 0; i < priceArray.length; i++) {
		int startPrice = priceArray[i];
		for (int j = i + 1; j < priceArray.length; j++) {
			int endPrice = priceArray[j];
			int difference = endPrice - startPrice;
			if (difference > maxDifference) {
				maxDifference = difference;
				startDay = i;
				endDay = j;
			}
		}
	}
	System.out.println("第" + startDay + "天买入,第" + endDay + "天卖出可获得最大差价:" + maxDifference);
}

运行结果:第7天买入,第11天卖出可获得最大差价:43

 

3. 最大子数组问题

    穷举法(Exhaustive method)虽简单,但往往不是最优方法。那有没有更好的方法呢?

    我们从一个稍微不同的角度来看待输入的数据。我们的目的是寻找一段时间,使得第一天到最后一天的股价净变化最大。因此我们不再从每日价格的角度去看待输入的数据,而是考察每日价格的变化。如果把每日价格的变化集合到数组array,如下图,那么该问题就变成了:在array中找出一个子数组(即最大子数组),使得子数组的所有元素之和最大

乍一看,这种问题的转化并没有给我们带来什么好处。如果采用暴力破解方法对于一段n天的时间,我们仍然要检查 次,运算时间仍为θ(n2)。

    接下来,我们寻找求最大子数组的更高效方法。需要提前说明的是,最大子数组有些时候可能有多个;只有当数组内包含负元素时,讨论最大子数组问题才有意义,因为对于非负数组,整个数组必然是最大子数组。

 

4. 采用分治法(Divide and Conquer)求解最大子数组

    在第一篇中我们已经接触过分治策略,它需要考虑以下三步:

    ① 分解:分解意味着我们要将数组分为两个规模尽量相等的子数组。也就是说,如果数组a[low~high]为将要求解的数组,我们要将a[low~high]分为a1[low~mid],a2[mid~high],其中mid = (high – low) / 2。

    ② 解决:解决意味着我们要递归的解决子数组的求解。这里需要注意的是,最大子数组所处的位置可能有以下三种情况:在a1中;在a2中;跨越中点。

    ③ 合并:通过比较以上三种情况求出的最大子数组,得出真正的最大子数组。

    下面给出分治法(Divide and Conquer)的Java实现代码:

/**
 * 分治法
 *
 * @param args
 */
public static void main(String[] args) {
	int[] differenceArray = new int[] { //
			13, -3, -25, -20, //
			-3, -16, -23, 18, //
			20, -7, 12, -5, //
			-22, 15, -4, 7, //
	};
	Result result = findMaxSubarray(differenceArray, 0, differenceArray.length - 1);
	System.out.println("第" + result.startDay + "天买入,第" + result.endDay + "天卖出可获得最大差价:" + result.maxDifference);
}

/**
 * 找出最大子数组
 *
 * @param array
 * @return
 */
private static Result findMaxSubarray(int array[], int low, int high) {
	if (low == high) {
		Result result = new Result();
		result.startDay = low;
		result.endDay = low;
		result.maxDifference = array[low];
		return result;
	} else {
		Result leftResult = findMaxSubarray(array, low, (high + low) / 2);
		Result rightResult = findMaxSubarray(array, (high + low) / 2 + 1, high);
		Result crossingMiddleResult = findMaxSubarrayCrossingMiddle(array, low, high);
		int maxDifference = Math.max(Math.max(leftResult.maxDifference, rightResult.maxDifference),
				crossingMiddleResult.maxDifference);
		if (maxDifference == leftResult.maxDifference) {
			return leftResult;
		} else if (maxDifference == rightResult.maxDifference) {
			return rightResult;
		} else {
			return crossingMiddleResult;
		}
	}
}

/**
 * 找出跨越中点的最大子数组
 *
 */
private static Result findMaxSubarrayCrossingMiddle(int[] array, int low, int high) {
	Result result = new Result();
	int maxLeftSum = Integer.MIN_VALUE;
	int maxRightSum = Integer.MIN_VALUE;
	int sum = 0;
	int mid = (high + low) / 2;
	for (int i = mid; i >= 0; i--) {
		sum += array[i];
		if (sum > maxLeftSum) {
			maxLeftSum = sum;
			result.startDay = i;
		}
	}
	sum = 0;
	for (int i = mid; i < array.length; i++) {
		sum += array[i];
		if (sum > maxRightSum) {
			maxRightSum = sum;
			result.endDay = i;
		}
	}
	result.maxDifference = maxLeftSum + maxRightSum;
	return result;
}

static class Result {
	public int startDay;
	public int endDay;
	public int maxDifference;
}

结果为:第7天买入,第11天卖出,可获得最大差价:43

    下面我们建立一个递归式来描述递归过程的运算时间。

    ① 对于low = high的基本情况(base case),花费常量时间θ(1)。

    ② 对于low ≠ hight的递归情况(recursive case),两次递归花费2T(n / 2)时间,findMaxSubarrayCrossingMiddle方法花费时间θ(n),其余花费常量时间θ(1),因此一共花费2T(n / 2) + θ(1)。即:

解得T(n) = θ(nlgn)。在n较大时,该方法将打败穷举法(Exhaustive method)

 

5. 另一种时间为线性的方法

    我们考虑这么一种方法:从数组的最左边开始向右扩展,记录扩展过程中的最大子数组。若已知array[0~j]的最大子数组,基于如下方法将解扩展为array[0~j+1]的最大子数组:array[0~j+1]的最大子数组要么是array[0~j]的最大子数组,要么是array[i~j+1](0≤i≤j+1)。在已知array[0~j]的最大子数组的情况下,可以在线性时间内找出形如array[i~j+1]的最大子数组。

ps:以上内容均摘自《算法导论》中文译本。本人只是提取出文中个人认为比较重要的点 加入了一些个人理解仅供参考。

时间: 2024-10-26 08:35:21

分治策略——算法导论(3)的相关文章

(学习6)特殊的分治策略算法——BFPTR

问题引出:给出一个集合N,求出其中第k小的数,第K小的元素指对集合L中的元素升序排列好后第K的元素. 1:惯性思维是对该集合中的每个数进行排序,然后找到索引为k的元素,最好的情况应该是O(nlogn) 2:BFPTR算法,一个即使是最坏情况下,也能达到O(n)的算法,通过对这个算法的学习,很直观的感受到,这个算法首先将集合中的数分5个一组,然后找到每组的中位数,将中位数放入一个集合,然后再求这个集合的中位数m,然后按照该中位数对数组进行划分,左边放比m小的,右边放比m大的,然后观察m的位置与k的

算法导论第四章分治策略编程实践(二)

在上一篇中,通过一个求连续子数组的最大和的例子讲解,想必我们已经大概了然了分治策略和递归式的含义,可能会比较模糊,知道但不能用语言清晰地描述出来.但没关系,我相信通过这篇博文,我们会比较清楚且容易地用自己的话来描述. 通过前面两章的学习,我们已经接触了两个例子:归并排序和子数组最大和.这两个例子都用到了分治策略,通过分析,我们可以得出分治策略的思想:顾名思义,分治是将一个原始问题分解成多个子问题,而子问题的形式和原问题一样,只是规模更小而已,通过子问题的求解,原问题也就自然出来了.总结一下,大致

分治策略(2)&mdash;&mdash;算法导论(4)

1. 引言     这一篇博文首先会介绍基于分治策略的矩阵乘法的Strassen算法,然后会给出几种求解递归式的方法.   2. 矩阵乘法的Strassen算法 (1) 普通矩阵乘法算法     矩阵乘法的基本算法的计算规则是:         若A=(aij)和B=(bij)是n×n的方阵(i,j = 1,2,3...),则C = A · B中的元素Cij为:     下面给出Java实现代码: public static void main(String[] args) { int[][]

算法导论 第四章 分治策略

分治策略中,我们递归地求解了一个问题,在每层递归都应用了三步 1.分解,将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小 2.解决,递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解 3.合并,把子问题的解给合并为原问题的解 当子问题足够大的时候,需要递归,那就是递归情况 当问题足够小的时候,不需要递归,那就是基本情况 三种求解递归式的方法:代入法 猜测一个界,用数学归纳法来证明这个界 递归树法 将递归式转化为一棵树,其节点表示不同层次的递归调用产生的代价,然后采用边

【经典算法】分治策略

一.什么是分治 有很多算法是递归的:为了解决一个给定的问题,算法要一次或多次递归调用其自身来解决的子问题.这些算法通常采用分治策略:将原问题划分为n个规模较小而结构与原问题相似的子问题:递归地解决这些子问题,然后再合并其结果,就得到原问题的解. 二.分治算法的三个步骤 分治模式在每一层递归上都有三个步骤: 分解(Divide)步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小. 解决(Conquer)步骤递归地求解出子问题.如果子问题规模足够小,则停止递归,直接求解. 合并(Co

【从零学习经典算法系列】分治策略实例——快速排序(QuickSort)

在前面的博文(http://blog.csdn.net/jasonding1354/article/details/37736555)中介绍了作为分治策略的经典实例,即归并排序,并给出了递归形式和循环形式的c代码实例.但是归并排序有两个特点,一是在归并(即分治策略中的合并步骤)上花费的功夫较多,二是排序过程中需要使用额外的存储空间(异地排序算法<out of place sort>). 为了节省存储空间,出现了快速排序算法(原地排序in-place sort).快速排序是由东尼·霍尔所发展的一

【从零学习经典算法系列】分治策略实例——二分查找

1.二分查找算法简介 二分查找算法是一种在有序数组中查找某一特定元素的搜索算法.搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束:如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较.如果在某一步骤数组 为空,则代表找不到.这种搜索算法每一次比较都使搜索范围缩小一半.折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn). 二分查找的优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且

第四章 分治策略 4.1 最大子数组问题 (暴力求解算法)

/** * 最大子数组的暴力求解算法,复杂度为o(n2) * @param n * @return */ static MaxSubarray findMaxSubarraySlower(int[] n) { long tempSum = 0; int left = 0; int right = 0; long sum = Long.MIN_VALUE; for (int i = 0; i < n.length; i++) { for (int j = i; j < n.length; j++

算法导论三剑客之 分治算法 合并排序

1 #include "iostream" 2 #include "windows.h" 3 #define MAX 0x7fffffff 4 using namespace std; 5 6 void merge(int s,int q,int e,int A[]){ 7 int i,j,k; 8 int B[100],C[100]; 9 for(i=s;i<=q;i++) 10 B[i-s+1]=A[i]; 11 for(j=q+1;j<=e;j++