算法导论-分而治之篇
最大子数组问题
分治法
把一个问题分成(同类的)几个子问题。
递归地解决(征服)每个子问题。
将子问题的解决方案组合成整体解决方案。
通常的使用
将大小为n的问题分成两个大小为n / 2的子问题。
递归求解(攻克)两个子问题。
将两个方案组合成整体方案。
当子问题足够大以进行递归求解时,称其为递归情况(recursive case)
一旦子问题变得很小,以至不再需要递归的程度,就说递归“结束(bottom out)”了,已经回到了基本情况(base case)
最大子数组(Max Subarray)问题,是计算机科学与技术领域中一种常见的算法问题,主要可以利用分治思想进行快速实现。
最大子数组问题描述如下:假如我们有一个数组,数组中的元素有正数和负数,如何在数组中找到一段连续的子数组,使得子数组各个元素之和最大。
1 分治法求解最大子数组问题
在最大子数组问题之后,我们一起来看一下如何利用分治思想求解最大子数组问题。这里我们假设待初始的数组为 [12, -3, -16, 20, -19, -3, 18, 20, -7, 12, -9, 7, -10],记为数组 A,并用 A [low,high] 表示这个数组,其中 low,high 是这个数组的最小最大下标, low = 0,high = A.length -1 , 然后我们需要找到该数组的其中某一个最大子数组。
Tips: 这里我们需要注意,同一数组的最大子数组可能有多个,所以我们在这里求解的时候只说求解某一个最大子数组。
1.1 分治算法求解思路
本部分完全参考自这篇文章
https://blog.csdn.net/mukewangguanfang/article/details/128828639
在这里,我们用分治算法求解最大子数组问题,主要思路如下:
步骤 1:
找到数组 A 的中间元素,其下标记为 mid,根据分治策略,将数组 A [low,high] 根据中间元素划分为 A [low,mid], A [mid+1,high] 两个部分;
步骤 2:
假设数组 A 的最大子数组为 A [i, j],那么 A [i, j] 只有以下三种可能:
a: 最大子数组 A [i, j] 完全位于 A [low, mid] 中,此时有 low <= i <= j <= mid;
b: 最大子数组 A [i, j] 完全位于 A [mid+1, high] 中,此时有 mid+1 <= i <= j <= high;
c: 最大子数组 A [i, j] 跨域了中间元素,则 low <= i <= mid <= j <= high。
分别计算上述三种对应的最大子数组的结果;
Tips: 在这里,情况 a 和情况 b 这两种情况所得的子问题和之前求解数组 A 的最大子数组的问题形式完全一样,这里是分治思想的主要体现,将大的问题拆分成了两个相同形式的小问题;情况 c 这时候可以直接求解,在 3.2 节中会具体介绍其求解过程。
步骤 3
对步骤 2 三种情况的求解结果进行比较,其中最大子数组的结果为最大值的情况就是我们的所求结果。
1.2 代码实现
#include <iostream>
#include <cmath>
using namespace std;
// 这个跨越中线的解一定是[l,mid]与[mid+1, r]组成
int findMaxCrossSubarray(int arr[], int l, int r)
{
if (l == r)
return arr[l];
else
{
int m = (l + r) / 2;
int left = m, right = m + 1;
int maxLeftSum = arr[m], maxRightSum = arr[m + 1];
int leftSum = 0, rightSum = 0;
for (int i = m; i >= l; i--)
{
leftSum += arr[i];
if (maxLeftSum < leftSum)
{
maxLeftSum = leftSum;
left = i;
}
}
for (int i = m + 1; i <= r; i++)
{
rightSum += arr[i];
if (maxRightSum < rightSum)
{
maxRightSum = rightSum;
right = i;
}
}
return maxLeftSum + maxRightSum;
}
}
int findMaxSubarray(int arr[], int l, int r)
{
if (l == r)
return arr[l];
else
{
int m = (l + r) / 2;
int left = findMaxSubarray(arr, l, m);
int right = findMaxSubarray(arr, m + 1, r);
int mid = findMaxCrossSubarray(arr, l, r);
//选择最大的数输出
if (mid >= right && mid >= left)
return mid;
else if (right >= mid && right >= left)
return right;
else
return left;
}
}
int main()
{
int arr[] = {5, 4, -1, 7, 8};
cout << findMaxSubarray(arr, 0, 4);
return 0;
}
//测试数据来着
https://leetcode.cn/problems/maximum-subarray/
2 动态规划法解决最大子数组问题
此部分参考此文章
https://blog.csdn.net/qq_36445477/article/details/105394002
1)分析问题结构: dp的第一步为分析问题结构,找到该问题的最佳子结构性质:对n个元素的数组arr[n]求解最大子数组和maxSum(n), 首先去找 maxSum(n) 和 maxSum(n-1) 的关系。
假设我们已经知道前n-1个元素的最大子数组和 maxSum(n-1), 设前n个元素中以第n元素结尾的最大子数组和为 P(n) , 则
maxSum(n) = max{ maxSum(n-1), P(n) }
继续观察 P(n) 和 P(n-1) 的关系: 若以第n-1个元素结尾的最大子数组和 P(n-1) 加上第n个元素的和大于第n个元素(即P(n-1)>0),那么以第n个元素结尾的最大子数组和为 P(n) = P(n-1) + arr[n],否则 P(n) 就等于arrr[n] 第n个元素本身, 即:
P(n) = max{ P(n-1)+arr[n], arr[n] }
我们已经找到了maxSum(n)和maxSum(n-1)子问题的关系,这就是最大子数组和问题的最优子结构性质。
2)构造递推式: 我们通过分析最优子结构性质。已经得到了dp的递推式:
P(n) = max{ P(n-1)+arr[n], arr[n] }
maxSum(n) = max{ maxSum(n-1), P(n) }
3)初始化数组,自底向上求解: 构造两个一维数组 maxSum 和 P ; P[i]: 数组前i的元素中包含元素i的最大子数组和; maxSum[i]: 数组前i个元素中的最大子数组和。
设没有元素的数组最大子数组和为负无穷以便于求解,即初始化P[0] 和maxSum[0] 为负无穷,而后以 i := 1 to n,自底向上求解,得到 **maxSum[n]**的值即为最大子数组和
代码部分如下
#include <iostream>
using namespace std;
int DPgetMaxSubarray(int arr[], int l, int r)
{
// 获得前n个元素以第n元素结尾的最大子数组和为 P(n)
int p[20];
int maxSum[20];
p[0] = arr[0];
maxSum[0] = arr[0];
for (int i = 1; i < r - l + 1; i++)
{
if (p[i - 1] + arr[i] > arr[i])
p[i] = p[i - 1] + arr[i];
else
p[i] = arr[i];
}
for (int i = 1; i <= r - l; i++)
{
if (maxSum[i - 1] > p[i])
maxSum[i] = maxSum[i - 1];
else
maxSum[i] = p[i];
}
return maxSum[r];
}
int main()
{
int arr[] = {5, 4, -1, 7, 8};
cout << DPgetMaxSubarray(arr, 0, 4);
return 0;
}