还不会做最大子段和?

简介: 最大子段和一定是每个(准)程序员都接触过的问题,题目很简洁:给出一个长度为 n 的序列a,选出其中连续且非空的一段使得这段和最大。为什么每个(准)程序员都需要掌握呢?首先这问题解法很多,时间复杂度从O(n3) -> O(n2) -> O(nlog2n) -> O(n)。通过解决这个问题可以体会到不同算法的效率差异,其次几种解法中包含分治、前缀和、贪心……等很多算法思想,深入理解最大子段和问题可以举一反三,映射到许多其他问题。

最大子段和


  最大子段和一定是每个()程序员都接触过的问题,题目很简洁:给出一个长度为 n 的序列a,选出其中连续且非空的一段使得这段和最大。为什么每个()程序员都需要掌握呢?首先这问题解法很多,时间复杂度从O(n3) -> O(n2) -> O(nlog2n) -> O(n)。通过解决这个问题可以体会到不同算法的效率差异,其次几种解法中包含分治、前缀和、贪心……等很多算法思想,深入理解最大子段和问题可以举一反三,映射到许多其他问题。


1.暴力 —— O(n3)


最容易想到的方法——枚举,然后就是代码五分钟,运行两小时

int max = a[0]; // 初始化最大值,不要写 max = 0,因为最大子段和一定为正?
for (int i = 0; i < n; ++i) { // 枚举起始位置
for (int j = i; j < n; ++j) { // 枚举由起始位置开始的子段
int sum = 0; // 当前子段和初始化
for (int k = i; k <= j; ++k) { // 计算当前子段和
sum += a[k];
}
if (sum > max) { // 更新最大子段和
max = sum;
}
}
}


2.暴力 or 前缀和 —— O(n2)


  • 上述代码中有很大优化空间,其中最内层循环多次重复计算,每次子段和都只增加一个元素,前面的子段和任然可用,所以可以进行优化。
int max = a[0]; // 初始化最大值,不要写 max = 0,因为最大子段和一定为正?
for (int i = 0; i < n; ++i) { // 枚举起始位置
int sum = 0; // 当前子段和初始化
for (int j = i; j < n; ++j) { // 计算当前子段和
sum += a[j]; // 每次在前一子段和基础上增加一个元素
if (sum > max) { // 更新最大子段和
max = sum;
}
}
}


  • 使用前缀和,设 Si = A1 + A2 + ··· + Ai,其中 Si就是叫做位置 i 的前缀和。则 Ai + Ai+1 + ··· + Aj     = Sj - Si-1,其直观含义是连续子序列之和等于两个前缀和之差。我们可以用这个结论改进 O(n3) 方法的最内层循环。
int max = a[0]; // 初始化最大值,不要写 max = 0,因为最大子段和一定为正?
for (int i = 0; i < n; ++i) { // 枚举起始位置
int sum = 0; // 当前子段和初始化
for (int j = i; j < n; ++j) { // 计算当前子段和
sum = i>0?x[j]-x[i-1]:x[j]; // x为前缀和数组,两个前缀和之差,i-1需要大于0,防止数组越界访问
if (sum > max) { // 更新最大子段和
max = sum;
}
}
}


3.分治 —— O(nlog2n)


上述算法虽然优化了内部循环,但是时间复杂度任然不如人意,所以我们尝试使用分治法来解决这个问题。


  • 将问题划分为两个规模相同(相近)的子问题。
  • 递归解决子问题(分别求出完全位于左半或完全位于右半的最大子段和)
  • 合并子问题的解得到原问题的解(最大子段和将有三种情况——完全位于左半、包含分界点、完全位于右半)
public static int f(int[] a, int x, int y) {
if (y-x == 1) {// 只有一个元素,直接返回
return a[x];
}
int mid = x+(y-x)/2; // 分治第一步,划分子问题
int max1 = Math.max(f(a, x, mid), f(a, mid, y));// 分治第二步,递归求解子问题
// 分治第三步,合并子问题的解
int sum = 0;
int max2 = a[mid-1];
for (int i = mid-1; i >= x; --i) {// 从分界点开始往左的最大连续和
sum += a[i];
max2 = Math.max(sum, max2);
}
sum = 0;
int max3 = a[mid];
for (int i = mid; i < y; ++i) {// 从分界点开始往右的最大连续和
sum += a[i];
max3 = Math.max(sum, max3);
}
return Math.max(max1, max2+max3);// 三种情况中的最优解(最大和)
}


4.特殊解法 or 前缀和贪心 —— O(n)


  • 最大子段和期望以大于 0 的数据开始,所以如果当前位置之前子段和为负则认为重新选择子段头较为合适,从 0 开始计算子段和,记录最大子段和并不断更新。
public static void main(String[] args) throws IOException {
Scanner in = new Scanner(System.in);
int n = in.nextInt();// 元素个数
int sum = 0;// 当前子段和
int max = Integer.MIN_VALUE;// 最大子段和,不应初始化为 0,因为最大子段和有可能为负
for (int i = 0; i < n; ++i) {// 动态处理
int a = in.nextInt();// 读取数据
sum = sum>0?sum:0;// 最大子段和期望以大于 0 开始,小于 0 则重置
sum += a;// 继续计算子段和
max = max>sum?max:sum;// 更新最大子段和
}
System.out.println(max);// 输出结果
in.close();
}


  • 利用前缀和贪心,Ai + Ai+1 + ··· + Aj = Sj     - Si-1,当 j 确定的时候,Sj - Si-1最大等价于 Si-1最小,因此只需要扫描一次数组,维护当前为止遇到过的最小 S 即可(如下代码包含完整输入输出,其中 nextInt() 为自写快读函数,对于大量数据读入更友好,详情见一次由 Scanner(System.in) 引起的 TLE)。
import java.io.*;
import java.util.*;
public class Main {
public static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int nextInt() throws IOException {
in.nextToken();
return (int)in.nval;
}
public static void main(String[] args) throws IOException {
int n = nextInt();// 快速读数据
int sum = 0; // 记录当前前缀和
int max = Integer.MIN_VALUE; // 记录最大子段和
int min = 0; // 记录当前位置之前最小前缀和
for (int i = 0; i < n; ++i) {// 读取数据同时计算前缀和
sum += nextInt(); // 读取数据同时计算当前前缀和
max = Math.max(max, sum-min); // 当前最大子段和等于当前前缀和减去最小前缀和
min = Math.min(min, sum); // 计算最小前缀和
}
System.out.println(max);
}
}
相关文章
一个求公约数和公倍数的有趣求法
一个求公约数和公倍数的有趣求法
56 0
|
算法 Java 索引
最长回文子串
最长回文子串
120 0
最长回文子串
|
人工智能
洛谷P1115-最大子段和(DP-最大子段和)
洛谷P1115-最大子段和(DP-最大子段和)
洛谷P1115-最大子段和(DP-最大子段和)
|
人工智能 算法
【动态规划】最大子段和
输入一个整型数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。要求时间复杂度为O(n)。例如输入的数组为1,-2,3,10,-4,7,2,-5,和最大的子数组为3,10,-4,7,2,因此输出为该子数组的和18。
215 0
最大子数组
最大子数组
71 0
|
算法 C语言
最大子序列和的问题的解(1)
最大子序列和的问题的解(1)
124 0
最大子序列和的问题的解(1)
|
人工智能 机器学习/深度学习