还不会做最大子段和?

简介: 最大子段和一定是每个(准)程序员都接触过的问题,题目很简洁:给出一个长度为 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);
}
}
相关文章
|
机器学习/深度学习 算法 Python
机器学习:归一化
这段内容主要讨论了归一化的目的和两种类型的归一化方法。归一化是为了确保在梯度下降过程中,不同维度的参数以相似的幅度调整,避免因数据尺度差异导致的优化问题。文中提到了最大值最小值归一化和标准归一化,后者更不易受到离群值的影响,并且可以使数据符合正态分布。通过Python代码示例展示了如何使用`StandardScaler`进行标准归一化。
467 2
|
SQL Oracle 关系型数据库
Oracle查询优化-计算字符在字符串中出现的次数
【2月更文挑战第3天】【2月更文挑战第7篇】只接上SQL
266 0
|
自然语言处理 算法 数据挖掘
自蒸馏:一种简单高效的优化方式
背景知识蒸馏(knowledge distillation)指的是将预训练好的教师模型的知识通过蒸馏的方式迁移至学生模型,一般来说,教师模型会比学生模型网络容量更大,模型结构更复杂。对于学生而言,主要增益信息来自于更强的模型产出的带有更多可信信息的soft_label。例如下右图中,两个“2”对应的hard_label都是一样的,即0-9分类中,仅“2”类别对应概率为1.0,而soft_label
自蒸馏:一种简单高效的优化方式
|
机器学习/深度学习 分布式计算
ICLR 2024:首个零阶优化深度学习框架
【2月更文挑战第28天】ICLR 2024:首个零阶优化深度学习框架
547 1
ICLR 2024:首个零阶优化深度学习框架
|
11月前
|
数据采集 存储 监控
数据治理:解锁数据资产潜力,驱动企业决策与业务增长的密钥
在当今这个数据驱动的时代,企业所拥有的数据资产已成为其核心竞争力的重要组成部分。然而,仅仅拥有海量数据并不足以确保成功,关键在于如何有效地管理和利用这些数据,以支持精准决策、优化运营流程并推动业务持续增长。这就是数据治理的重要性所在——它是一套系统性的方法和流程,旨在确保数据质量、安全性、可用性和合规性,从而让数据资产能够最大化地支持企业决策和业务增长。
|
11月前
|
存储 自然语言处理
LangChain-04 RAG Retrieval-Augmented Generation 检索增强生成
LangChain-04 RAG Retrieval-Augmented Generation 检索增强生成
147 3
|
数据库 数据安全/隐私保护 数据库管理
基于SpringBoot+Vue健身房管理系统(源码+部署说明+演示视频+源码介绍)(2)
基于SpringBoot+Vue健身房管理系统(源码+部署说明+演示视频+源码介绍)
261 1
|
存储 运维 Serverless
函数计算产品使用问题之如何规避因提高CPU规格而导致的内存规格不必要增加的问题
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
151 0
|
JavaScript 前端开发
JavaScript的`call`方法:实现函数间的调用!
JavaScript的`call`方法:实现函数间的调用!
Linux设备深探:桥接硬件与软件的秘密通道
在Linux的世界里,"设备"这个词汇比你想象的要丰富和多彩得多。让我们一起来探索Linux设备的奥秘,理解它们是如何在Linux操作系统中发挥作用的。🐧✨
Linux设备深探:桥接硬件与软件的秘密通道