zoukankan      html  css  js  c++  java
  • 分治策略(1)——算法导论(3)

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

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

    image

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

    image

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

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

    我们最容易想到的方法应该是暴力求解方法(事实上在条件允许的时候,暴力破解方法不失为是一种好方法)。分析该方法可发现,n 天中我们需要尝试的组合将有image 种,而处理每对日期的时间至少是常量时间,因此,这种方法的运行时间是θ(n²)。下面给出这种算法的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中找出一个子数组(即最大子数组),使得子数组的所有元素之和最大。

    image

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

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

    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;
    }

    代码很简单,就不一一细说了。你可能对如何找出跨越中点的最大子数组有疑问。下面解释一下,在此之前,先形式化地描述一下我们的问题:

    假设我们所求的数组为 A,其长度是 n + 1(下标范围是 0~n)。我们用 Aij 来表示数组 A 下标范围为 i ~ j 的子数组。再记 m 是数组 A 的中点下标,现在问题变为,找出子数组 Aij(0 <= i <= m,m <= j <= n,使得 Aij 是最大子数组。

    上述 findMaxSubarrayCrossingMiddle 方法的编写基于以下事实:要想确定问题中的 i 和 j,只需保证 Aim 是 A0m 中的最大子数组,Amj 是 Amn 中的最大子数组即可。而找出 Aim 和 Amj 是非常简单的,如 findMaxSubarrayCrossingMiddle 方法中操作即可。为什么是这样呢?我们用反证法很好证明:假设上述方法找出的 i 所确定的 Aij 不是最大子数组,即存在 i' (0 < i' < m),使得 Ai'j 才是最大子数组。那么对比 Aij 和 Ai'j ,Ai'm 之和比 Aim 大,但这与 Aim 是 A0m 中的最大子数组这一事实相矛盾,因此假设不成立,这就是说 i 是 我们想要寻找的最大子数组的其实下标;对 m 的证明同理。

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

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

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

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

    image

    解得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]的最大子数组。

    实现代码略。

  • 相关阅读:
    Microjs: 超棒的迷你框架和迷你类库搜罗工具
    本周推荐7款CSS3实现的动态特效
    Bootstrap3.1开发的响应式个人简历模板
    10分钟,利用canvas画一个小的loading界面
    四款超棒的jQuery数字化签名插件
    搜索引擎优化网页设计:最佳实践
    推荐超实用的8款jQuery插件
    9款HTML5实现的超酷特效
    想成为程序猿?28个在线学习网站让你变身齐天大圣!
    推荐7款超棒的单页面网站设计模板。关键是!免费!!
  • 原文地址:https://www.cnblogs.com/dongkuo/p/4800949.html
Copyright © 2011-2022 走看看