一、前言
《算法导论》第四章 分治策略 ,4.1最大子数组问题
二、题目
有一只股票你被准许可以在某个时刻买进一股该公司的股票,并在之后某个日期将其卖出,买进卖出都是在当天交易结束后进行。为了补偿这一限制,你可以了解股票将来的价格。你的目标是最大化收益。下图4-1给出了17天内的股票价格。
三、思路
有三种解法:
解法一:《算法导论》书中解法。
我们的目的是寻找一段日期,使得从第一天到最后一天的股票价格净变值最大。因此,我们不再从每日价格的角度去看待输入数据,而是考察每日价格变化,第 i 天的价格变化定义为第 i 天和第 i-1天的价格差。图4-3表格的最后一行给出了每日价格变化。如果将这一行看做一个数组A,如图4-3所示,那么问题就转化为寻找A的和最大的非空连续子数组。我们称这样的连续子数组为最大子数组(maxinum subarray)。例如,对图4-3中的数组,A[1.. 16]的最大子数组为A[8.. 11],其值和为43。
使用分治策略的求解方法
我们来思考如何用分治技术来求解最大子数组问题。假定我们要寻找子数组 A[low.. high]的最大子数组。使用分治技术意味着我们要将子数组划分为两个规模尽量相等的子数组。也就是说,找到子数组的中央位置,比如mid,然后考虑求解两个子数组 A[low..mid]和 A[mid+1.. high]。如图4-4(a)所示,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[lowe..high] 的一个最大子数组必然是完全位于 A[low..mid] 中、完全位于 A[mid+ 1..high] 中或者跨越中点的所有子数组中和最大者。我们可以递归地求解 A[low..mid] 和 A[mid+1..high]的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此,剩下的全部工作就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。
时间复杂度 O(nlgn)
解法二:用栈辅助求解
对于任何一天买入,想收益最大,只需要找到之后的某一天有最高的价格,就能得到最大收益,然后找出所有天数中最大收益就是我们需要的结果。转换之后是不是似曾相识,前几天做过【包含min函数的栈】,【包含min函数的栈】是求最小,这里是最大,是不是可以用这种思路。
我们再想想这个问题,先转换为数学模型,f(n):在第n买入可以得到的最大效益, g(n):第n天的价格。 f(n) = g(k) - g(n);(k >= n 且 第k天是 第n天之后的最大值)。 f(n) = g(k) - g(n) 等价于 f(n) = g(n) - g(d)(d <= n 且 第d天是 第n天之后的最小的值)。求f(n) 和 【包含min函数的栈】思路一样。最大化收益 = max(f(n))。闲谈:当昨天晚上看到这个问题的时候,分析完题型之后,立马联想到前不久做的一道题【包含min函数的栈】,心中喜悦却无法和他人分享。
这种解法时间复杂度是O(n)。
包含min函数的栈 https://blog.youkuaiyun.com/nie2314550441/article/details/105717656
解法三:一次遍历完美解决
分析完解法二,我们发现最优解就是找到一个最小值,和它后面的一个最大值,且差值是当前最大值即为最大化收益值。时间复杂度是O(n)。
四、闲谈
这个问题给出了三种解法,第三种解法是第二种解法延伸(优化),当我们处理一个问题的时候思路有开阔,当我们深入到问题解决方案细节的时候,有时候会收获到意想不到的惊喜。
当我们分析这个问题的思路的时候,之前的解决方案可能会给我们带来灵感,例如【包含min函数的栈】的求解。这应该是做算法题给我们带来的好处。
后面两种解决方案效率很高,当第一种解决方案用到的分治策略,值得我们好好回味。后面两种解决方案算是对特定问题的解答,而分治策略却是可以为一个类型的问题提供了解决思路。
再想一个问题:一个数组f(n),求任意连续数组和最大值,例如 f(3) + f(4) + f(5) 是数组3-5的和。这个问题是不是就是解法一需要求解的问题,那我们是不是可以把它转换成用解法二或解法三来解决了。
如果将加改成乘积呢? 《乘积最大子数组》https://blog.youkuaiyun.com/nie2314550441/article/details/106206639
五、编码实现
#include<iostream>
#include<vector>
#include<stack>
using namespace std;
// 解法一 start ///
// 查找夸中间的最大子数组
void FindMaxCrossingSubarray(int arr[], int low, int mid, int hight, int& start, int& end, int& sum)
{
if (low > mid || mid > hight || hight < 0)
{
cout << "参数错误" << endl;
return;
}
int leftMaxSum = INT_MIN;
int lSum = 0;
start = mid;
for (int i = mid; i >= low; i--)
{
lSum += arr[i];
if (lSum > leftMaxSum)
{
leftMaxSum = lSum;
start = i;
}
}
int rightMaxSum = INT_MIN;
int rSum = 0;
end = mid;
for (int i = mid + 1; i <= hight; i++)
{
rSum += arr[i];
if (rSum > rightMaxSum)
{
rightMaxSum = rSum;
end = i;
}
}
sum = leftMaxSum + rightMaxSum;
}
// 查找最大子数组
void FindMaximumSubarray_Solution1_Operation(int arr[], int low, int hight, int &start, int &end, int &sum)
{
if (low > hight || hight < 0)
{
cout << "参数错误" << endl;
return;
}
if (low == hight)
{
start = low;
end = hight;
sum = arr[start];
return;
}
int mid = (low + hight) / 2;
int lStart = 0, lEnd = 0, lSum = 0;
FindMaximumSubarray_Solution1_Operation(arr, low, mid, lStart, lEnd, lSum); //位于左子数组
int rStart = 0, rEnd = 0, rSum = 0;
FindMaximumSubarray_Solution1_Operation(arr, mid+1, hight, rStart, rEnd, rSum); //位于右子数组
int mStart = 0, mEnd = 0, mSum = 0;
FindMaxCrossingSubarray(arr, low, mid, hight, mStart, mEnd, mSum); //跨中间子数组
if (lSum >= rSum && lSum >= mSum)
{
start = lStart; end = lEnd; sum = lSum;
}
else if (rSum >= lSum && rSum >= mSum)
{
start = rStart; end = rEnd; sum = rSum;
}
else
{
start = mStart; end = mEnd; sum = mSum;
}
}
// 查找最大子数组
void FindMaximumSubarray_Solution1(int arr[], int len, int& start, int& end, int& sum)
{
vector<int> varr;
for (int i = 1; i < len; ++i)
{
varr.push_back(arr[i] - arr[i - 1]);
}
FindMaximumSubarray_Solution1_Operation(varr.data(), 0, varr.size() - 1, start, end, sum);
end++; // 高位向左移了一位,需要+1
// 如果一直是跌,不买进
if (sum <= 0)
{
start = 0; end = 0; sum = 0;
}
}
// 解法一 end ///
// 解法二 start ///
// 查找最大子数组
void FindMaximumSubarray_Solution2(int arr[], int len, int& start, int& end, int& sum)
{
if (len <= 1)
{
cout << "参数错误" << endl;
return;
}
stack<int> dataStack;
stack<int> minStack;
for (size_t i = 0; i < len; i++)
{
dataStack.push(arr[i]);
if (minStack.empty() || arr[i] < minStack.top())
{
minStack.push(arr[i]);
}
else
{
minStack.push(minStack.top());
}
}
int tSum = INT_MIN;
int index = len - 1;
int minNum = INT_MIN;
while (!dataStack.empty() && !minStack.empty())
{
if (dataStack.top() - minStack.top() > tSum)
{
end = index;
tSum = dataStack.top() - minStack.top();
minNum = minStack.top();
}
if (dataStack.top() == minNum)
{
start = index;
}
--index;
dataStack.pop();
minStack.pop();
}
sum = tSum;
// 如果一直是跌,不买进
if (sum <= 0)
{
start = 0; end = 0; sum = 0;
}
}
// 解法二 end ///
// 解法三 start ///
// 查找最大子数组
void FindMaximumSubarray_Solution3(int arr[], int len, int& start, int& end, int& sum)
{
if (len <= 1)
{
cout << "参数错误" << endl;
return;
}
int minNum = INT_MAX;
int index = 0;
int maxNum = 0; //如果都是下跌就不买进
for (size_t i = 0; i < len; i++)
{
if (arr[i] < minNum)
{
start = i;
minNum = arr[i];
}
if (arr[i] - minNum > maxNum)
{
end = i;
maxNum = arr[i] - minNum;
}
}
sum = maxNum;
// 如果一直是跌,不买进
if (sum <= 0)
{
start = 0; end = 0; sum = 0;
}
}
// 解法三 end ///
// ====================测试代码====================
void test(const char* testName, int arr[], int len, int expectedStart, int expectedEnd, int expectedSum)
{
if (len <= 1)
{
cout << "参数错误" << endl;
return;
}
int start = 0, end = 0, sum = 0;
auto printret = [&](int SolutionId)
{
string ret = "FAILED";
if (expectedStart == start && expectedEnd == end && expectedSum == sum) { ret = "passed"; }
cout << testName << "\tSolution"<< SolutionId<<"\t" << ret << ".\t期望(" << expectedStart << ", " << expectedEnd << ", " << expectedSum << ") 结果(" << start << ", " << end << ", " << sum << ")" << endl;
};
// 解法一
FindMaximumSubarray_Solution1(arr, len, start, end, sum);
printret(1);
// 解法二
start = 0, end = 0, sum = 0;
FindMaximumSubarray_Solution2(arr, len, start, end, sum);
printret(2);
// 解法三
start = 0, end = 0, sum = 0;
FindMaximumSubarray_Solution2(arr, len, start, end, sum);
printret(3);
cout << endl;
}
void test1()
{
int arr[] = { 20, 30 };
int start = 0, end = 0, sum = 0;
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 0;
int expectedEnd = 1;
int expectedSum = 10;
test("test1", arr, len, expectedStart, expectedEnd, expectedSum);
}
void test2()
{
int arr[] = { 30, 20 };
int start = 0, end = 0, sum = 0;
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 0;
int expectedEnd = 0;
int expectedSum = 0;
test("test2", arr, len, expectedStart, expectedEnd, expectedSum);
}
void test3()
{
int arr[] = { 80, 70, 60, 50, 40, 30 };
int start = 0, end = 0, sum = 0;
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 0;
int expectedEnd = 0;
int expectedSum = 0;
test("test3", arr, len, expectedStart, expectedEnd, expectedSum);
}
void test4()
{
int arr[] = { 20, 30, 100, 50, 10, 80, 30 };
int start = 0, end = 0, sum = 0;
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 0;
int expectedEnd = 2;
int expectedSum = 80;
test("test4", arr, len, expectedStart, expectedEnd, expectedSum);
}
void test5()
{
int arr[] = { 20, 30, 100, 50, 10, 80, 1000 };
int start = 0, end = 0, sum = 0;
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 4;
int expectedEnd = 6;
int expectedSum = 990;
test("test5", arr, len, expectedStart, expectedEnd, expectedSum);
}
void test6()
{
int arr[] = { 100, 113, 110, 85, 105, 102, 86, 63, 81, 101, 94, 106, 101, 79, 94, 90, 97 };
int len = (sizeof(arr) / sizeof(arr[0]));
int expectedStart = 7;
int expectedEnd = 11;
int expectedSum = 43;
test("test6", arr, len, expectedStart, expectedEnd, expectedSum);
}
int main()
{
test1();
test2();
test3();
test4();
test5();
test6();
}
执行结果: