这是《算法导论》书中给的一个例子。下图给出了17天的股票价格。第0天的价格是100美元,你可以在此后任何时刻买进股票。人们都希望“低价买进,高价卖出”,并最大化收益。这里就是要求解在哪一天买进,哪一天卖出,可以获得的收益最大。
本文中,给出了暴力求解和分治法两种求解的方法。暴力求解方法就是尝试多有可能的买进卖出组合,找出收益最大的组合。
在分而治之的方法中,我们将原问题进行变换。即寻找一个日期段,在这个日期段的第一天买进,在这个日期段的最后一天卖出,这时获益最大。这样,我们要关心的就不再是每天股票的价格了,而是每天相对于前一天的价格变化量,如下图中的数组所示。
对于数组A[low…high],使用分治方法,意味着我们要将子数组划分为两个规模尽量相等的子数组。也就是说,找到数组的中央位置,比如mid。然后考虑两个子数组A[low…mid]和A[mid+1…high]。在A[low…high]中,任何连续子数组只有三种情况:
(1) 完全在左子数组A[low…mid]中;
(2) 完全在右子数组A[mid+1…high]中;
(3) 跨越了中点,一部分在A[low…mid]中,另一部分在A[mid+1…high]中。
因而,我们对于一个数组,可以在这三个情况中分别求解,选取其中最大一个。
用Java实现的暴力求解法和分而治之的方法代码如下所示。
/**
* 最大子数组问题
* 分别用暴力法和分治法求解
* @author sdu20
*
*/
public class MaxSubarray {
public static final int NEG_INF = -10000;//当做负无穷使用
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] shares = {100,113,110,85,105,102,86,63,81,101,94,106,101,79,94,90,97};
Result result1 = Brutefoce(shares);
System.out.println("暴力求解结果:");
System.out.println(result1.toString());
Result result2 = Divide(shares);
System.out.println("分而治之求解结果:");
System.out.println(result2.toString());
}
/**
* 暴力求解法
* @param array
* @return
*/
public static Result Brutefoce(int[] array){
int in = 0;
int out = 1;
int profit = array[1]-array[0];
for(int i = 0;i<array.length;i++){
for(int j = i+1;j<array.length;j++){
if(array[j]-array[i]>profit){
in = i;
out = j;
profit = array[j]-array[i];
}
}
}
return (new Result(in,out,profit));
}
/**
* 分而治之求解
* @param array
* @return
*/
public static Result Divide(int[] array){
int[] a2 = new int[array.length];
a2[0] = 0;
for(int i = 1;i<a2.length;i++){
a2[i] = array[i]-array[i-1];
}
Result result = Divide_Result(a2,1,a2.length-1);
result.left = result.left-1;//因为a2数组中存储的是价格比昨天的变化,因而买入的日期应该加一,同理上一行中的左索引为1而不是0
return result;
}
/**
* 分而治之的方法求解最大子数组
* @param array
* @param left 左索引
* @param right 右索引
* @return
*/
public static Result Divide_Result(int[] array,int left,int right){
if(left==right){
return (new Result(left,right,array[left]));
}
int mid = (left+right)/2;
Result left_result = Divide_Result(array,left,mid);
Result right_result = Divide_Result(array,mid+1,right);
Result mid_result = FIND_MAX_CROSSING_SUBARRAY(array,left,mid,right);
if(left_result.sum>=right_result.sum && left_result.sum>=mid_result.sum){
return left_result;
}
if(right_result.sum>=left_result.sum && right_result.sum>=mid_result.sum){
return right_result;
}
return mid_result;
}
/**
* 分治法
* 当子数组跨越中点时
* @param array 数组
* @param left 左边起始索引
* @param mid 中点
* @param right 右边终止索引
* @return
*/
public static Result FIND_MAX_CROSSING_SUBARRAY(int[] array,int left,int mid,int right){
int left_sum = NEG_INF;
int right_sum = NEG_INF;
int max_left_index = mid;
int max_right_index = mid+1;
int sum = 0;
for(int i = mid;i>=left;i--){
sum += array[i];
if(sum>left_sum){
left_sum = sum;
max_left_index = i;
}
}
sum = 0;
for(int i = mid+1;i<=right;i++){
sum += array[i];
if(sum>right_sum){
right_sum = sum;
max_right_index = i;
}
}
return (new Result(max_left_index,max_right_index,left_sum+right_sum));
}
}
/**
* 用于存储结果的数据结构
* @author sdu20
*
*/
class Result{
int left; //左索引
int right; //右索引
int sum; //和
public Result(int l,int r,int s){
this.left = l;
this.right = r;
this.sum = s;
}
public String toString(){
return "left index: "+this.left+" right index: "+this.right+" profit:"+this.sum;
}
}
运行结果如下所示
在书中练习4.1-5题中提到解决最大子数组问题还存在线性时间复杂度的算法。题目中提到了根据提到的已知A[i..j]的最大子数组,去计算A[i..j+1]的最大子数组的思路,但是这种方法只是在根据A[i..j]的最大子数组,去计算A[i..j+1]的最大子数组的过程的时间复杂度是线性的,但是总的时间复杂度仍然很高,并不比分治方法好。
我们可以考虑另一种思路。对于一个数组的最大子数组,在最大子数组不仅仅有一个元素的情况下,任意的将这个最大子数组切分成前后两个部分,这两部分各自的和必然是正的,也就是任意一半的和肯定小于总的和。根据这一点我们可以有如下的线性时间复杂度的算法代码实现:
public static void linearAlgorithm(int[] array){
int templeft = 0;
int tempright = 0;
int left = 0;
int right = 0;
int sum = array[0];
int maxsum = array[0];
for(int i = 1;i<array.length;i++){
if(sum+array[i]>array[i]){
sum = sum+array[i];
tempright++;
}else{
sum = array[i];
templeft = i;
tempright = i;
}
if(maxsum<sum){
maxsum = sum;
left = templeft;
right = tempright;
}
}
System.out.println("left index: "+left+"\t"+"right index: "+right+"\tprofit: "+maxsum);
}