
给出一个长度为 nn 的序列 aa,选出其中连续且非空的一段使得这段和最大。
输出一行一个整数表示答案。
输入 #1:
7
2 -4 3 -1 2 -4 3输出 #1:
4样例解释:
选取子数组 {3, −1, 2},其和为 4。
本问题的核心是求解一个数组中 连续子数组 的最大和。为了帮助大家更好地理解如何优化算法,我们将从最基础的暴力法出发,逐步介绍一些优化策略,最后给出最优解法。每一种算法都有其优点和局限,适用于不同规模的数据。
暴力法的基本思路非常简单:枚举数组中所有可能的子数组,计算它们的和,并找出最大的那个子数组和。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int a[n];
// 输入数组
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int ret = INT_MIN;
// 暴力法:枚举所有子数组
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += a[j]; // 累加子数组元素
ret = max(ret, sum); // 更新最大子段和
}
}
cout << ret << endl; // 输出最大子段和
return 0;
}前缀和优化的基本思想是通过 前缀和 数组来避免重复计算子数组的和。前缀和是一种空间换时间的技巧,它将每个子数组的和存储在一个数组中,查询时可以快速得到结果。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int a[N];
long long f[N]; // 前缀和
long long ret = INT_MIN;
int n;
int main() {
cin >> n;
// 输入数组并计算前缀和
for (int i = 1; i <= n; i++) {
cin >> a[i];
f[i] = f[i - 1] + a[i]; // 前缀和
}
// 枚举所有可能的子数组
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
ret = max(ret, f[j] - f[i - 1]); // 通过前缀和计算子数组和
}
}
cout << ret << endl;
return 0;
}分治法通过递归的方式,将问题分解成较小的子问题,并计算左右子数组和,最后合并结果。这种方法借鉴了“分而治之”的思想。
#include <bits/stdc++.h>
using namespace std;
int a[100000];
int n;
int dfs(int left, int right) {
if (left == right) return a[left]; // 基本情况,只有一个元素时返回其值
int mid = (left + right) / 2;
// 分别计算左右两部分的最大子段和
int left_max = dfs(left, mid);
int right_max = dfs(mid + 1, right);
// 计算跨越中点的最大子段和
int left_sum = 0, right_sum = 0;
int lmax = INT_MIN, rmax = INT_MIN;
for (int i = mid; i >= left; i--) {
left_sum += a[i];
lmax = max(lmax, left_sum);
}
for (int i = mid + 1; i <= right; i++) {
right_sum += a[i];
rmax = max(rmax, right_sum);
}
return max({left_max, right_max, lmax + rmax}); // 返回三者中的最大值
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
cout << dfs(0, n - 1) << endl;
return 0;
}贪心算法通过每一步选择最优的局部解,期望最终得到全局最优解。对于最大子段和问题,贪心算法选择每次更新当前子数组和,如果当前和为负,则从当前元素开始一个新的子数组。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int ret = INT_MIN, cur = 0;
for (int i = 0; i < n; i++) {
cur = max(a[i], cur + a[i]); // 如果当前和为负,则从当前元素开始
ret = max(ret, cur); // 更新最大子段和
}
cout << ret << endl;
return 0;
}普通动态规划通过记录每个位置的最大子数组和,并利用已计算的状态来更新当前状态。
dp[i] 表示以第 ii 个元素为结尾的最大子数组和。dp[i] = max(dp[i - 1] + a[i], a[i])。#include <bits/stdc++.h>
using namespace std;
int dp[100000]; // dp[i]表示以第i个元素结尾的最大子段和
int n;
int main() {
cin >> n;
int ret = INT_MIN;
cin >> dp[0]; // 初始化第一个元素的子数组和
ret = dp[0];
for (int i = 1; i < n; i++) {
int a;
cin >> a;
dp[i] = max(dp[i - 1] + a, a); // 当前子数组和最大值
ret = max(ret, dp[i]); // 更新全局最大子数组和
}
cout << ret << endl;
return 0;
}dp[] 数组。Kadane 算法是动态规划的一种优化,通过减少不必要的空间使用来提高效率。
cur(当前子数组和)和 ret(全局最大子数组和)。cur 为当前元素和 cur + a[i] 的较大值。cur 为负数,则从当前元素重新开始计算子数组和。#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int ret = INT_MIN, cur = 0;
for (int i = 0; i < n; i++) {
int a;
cin >> a;
cur = max(a, cur + a); // 如果当前和为负,则从当前元素开始
ret = max(ret, cur); // 更新最大子段和
}
cout << ret << endl;
return 0;
}算法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
暴力法 | O(n²) | O(1) | 简单直观,适用于小规模数据,但时间复杂度高。 |
前缀和优化 | O(n²) | O(n) | 通过前缀和优化暴力法,但依然时间复杂度较高,适用于小规模数据。 |
分治法 | O(n log n) | O(log n) | 递归优雅,适合复杂问题,但时间复杂度较高,空间开销大。 |
贪心算法 | O(n) | O(1) | 高效,适用于大规模数据,但不一定得到最优解。 |
普通动态规划 | O(n) | O(n) | 能处理大规模数据,但空间复杂度较高。 |
Kadane 算法 | O(n) | O(1) | 最优解法,适用于大规模数据,时间空间复杂度最低。 |
从表中可以看出,Kadane 算法是最优解法,适用于大规模数据。对于小规模数据,暴力法、前缀和优化和分治法可以作为参考,而贪心算法能高效解决问题,但并不适用于所有情况。