当前位置: 首页 > article >正文

最大子数组问题非蛮力算法

文章目录

  • 原题链接
  • 动态规划(Dynamic Programming) - Kadane算法
  • 前缀和
    • 用于解决变种题目
  • 分治法(Divide and Conquer)
  • 线段树(Segment Tree)

原题链接

53. 最大子数组和

动态规划(Dynamic Programming) - Kadane算法

  • 时间复杂度: O ( n ) O(n) O(n)
  • 思路:遍历数组,同时维护两个变量,一个是当前的最大子数组和,另一个是当前的最大子数组结束位置的连续和。在每一步更新这两个变量。
  • 代码
def solution(nums):
	res=nums[0]
	cur=res
	for i, num in enumerate(nums[1:]):
		cur=max(num, cur+num)
		res=max(res, cur)
	return res
  • 分析:给定数组的最大子数组满足最优子结构:将数组从右侧剪裁到以解结束的位置,这个解还是剪裁后的数组的最优解。这也说明最大子数组的最优解也是原问题某个子数组的最大后缀数组。枚举所有最大后缀数组即可。当我们遍历时,已知 nums [ i − 1 ] \text{nums}[i-1] nums[i1]的最大后缀数组, nums [ i ] \text{nums}[i] nums[i]的最大后缀数组可以通过状态转移获得:
    suffixs [ i ] = { nums [ i ] if nums [ i ] > suffix [ i − 1 ] + nums [ i ] , suffix [ i − 1 ] + nums [ i ] if suffix [ i − 1 ] + nums [ i ] > nums [ i ] . \text{suffixs}[i]= \begin{cases} \text{nums}[i] &\text{if } \text{nums}[i] >\text{suffix}[i-1]+\text{nums}[i], \\ \text{suffix}[i-1]+\text{nums}[i] &\text{if }\text{suffix}[i-1]+\text{nums}[i]>\text{nums}[i]. \end{cases} suffixs[i]={nums[i]suffix[i1]+nums[i]if nums[i]>suffix[i1]+nums[i],if suffix[i1]+nums[i]>nums[i].
    suffixs [ i ] = max ⁡ { nums [ i ] , suffix [ i − 1 ] + nums [ i ] } \text{suffixs}[i]=\max\{\text{nums}[i], \text{suffix}[i-1]+\text{nums}[i]\} suffixs[i]=max{nums[i],suffix[i1]+nums[i]}
    最大子数组值为 max ⁡ { suffixs [ i ] ∣ i } \max\{\text{suffixs}[i] | i\} max{suffixs[i]i}

前缀和

  • 时间复杂度: O ( n ) O(n) O(n)
  • 思路:先构建前缀和数组,然后遍历前缀和数组,任意子数组的和可通过前缀和数组的差分获得。同时维护两个变量,一个是当前的最大子数组和,一个是遍历过的最小前缀和。在每一步更新这两个变量。当前的最大子数组结束位置的连续和可以通过当前前缀和减最小前缀和获得。本质还是计算当前的最大子数组结束位置的连续和。
  • 优点是更为灵活。缺点是时间复杂度虽然渐进 O ( n ) O(n) O(n)但是由于多次遍历相比Kadane算法实际时间复杂度系数更高。
  • 代码
def solution(nums):
	prefixs=[nums[0]]
	for i, num in enumerate(nums[1:]):
		prefixs.append(prefixs[-1]+num)
	min_prefix=min(0, prefixs[0])
	res=prefixs[0]
	for i, prefix in enumerate(prefixs[1:]):
		res=max(res, prefix-min_prefix)
		min_prefix=min(min_prefix, prefix)
	return res

以下为常见错误代码:

def solution(nums):
	prefixs=[nums[0]]
	for i, num in enumerate(nums[1:]):
		prefixs.append(prefixs[-1]+num)
	min_prefix=prefixs[0]
	res=min_prefix
	for i, prefix in enumerate(prefixs[1:]):
		res=max(res, prefix-min_prefix)
		min_prefix=min(min_prefix, prefix)
	return res

最小前缀和的初始值应赋为0而不是前缀和的第一项,因为当前缀和中没有负数时,当前前缀和就是子数组的最大后缀数组和。

用于解决变种题目

周赛427-Q3: 长度可被K 整除的子数组的最大元素和

  • 代码
class Solution:
    def maxSubarraySum(self, nums: List[int], k: int) -> int:
        prefix_sum = []
        prefix_sum.append(nums[0])
        for i, num in enumerate(nums[1:]):
            prefix_sum.append(prefix_sum[-1] + num)
        memo = [float('inf')]*k  # 取余桶-保存不同位置的最小值
        memo[k-1]=0
        res=float('-inf')
        for i, pre_sum in enumerate(prefix_sum, start=0):
            if i>=k-1:
                res = max(res, pre_sum-memo[i%k])
            if pre_sum < memo[i%k]:
                memo[i%k] = pre_sum
        return res©leetcode

分治法(Divide and Conquer)

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 思路:将数组分为两半,最大子数组可能出现在以下三种情况之一:
    • 完全位于左半部分
    • 完全位于右半部分
    • 跨越中间点
  • 对于前两种情况,可以递归解决。对于第三种情况,可以从中间点向左右扩展,找到最大的子数组。
  • 代码
class Solution:
    
    def recursive(self, nums, lo, hi):
        if lo==hi-1:
            return nums[lo]
        mid = (lo+hi)//2
        l_max = -float("inf")
        r_max = -float("inf")
        l_sum = 0
        for i in range(mid-1, lo-1, -1):
            l_sum+=nums[i]
            l_max=max(l_max, l_sum)
        r_sum = 0
        for i in range(mid, hi):
            r_sum+=nums[i]
            r_max=max(r_max, r_sum) 
        l_r_max = max(self.recursive(nums, lo, mid), self.recursive(nums, mid, hi))
        return max(l_r_max, l_max+r_max)

    def maxSubArray(self, nums: List[int]) -> int:
        n=len(nums)
        self.dp=[[float("-inf")]*n for _ in range(n)]
        return self.recursive(nums, 0, len(nums))
  • 分析: 第三种情况从中间向两侧拓展并不需要检查所有起始与结束对( O ( n 2 ) O(n^2) O(n2)),只需要分别检查两侧的后缀数组与前缀数组的最大值并相加( O ( n ) O(n) O(n))。
    • 证明:假设横跨的最大子数组不是两个相加,则前后必有一部分大于最大的后缀数组或前缀数组,而此时找到了比最大的数组更大的数组,矛盾。因此横跨的最大子数组一定是后缀数组与前缀数组的最大值并相加。

线段树(Segment Tree)

  • 时间复杂度: O ( n ) O(n) O(n)
  • 思路:使用线段树来维护区间的和,可以快速查询和更新区间的最大子数组和。
  • 代码
class Status:
    def __init__(self, l_max, r_max, m_max, i_sum):
        self.l_max=l_max
        self.r_max=r_max
        self.m_max=m_max
        self.i_sum=i_sum

class Solution:

    def pushUp(self, l_status, r_status):
        l_max = max(l_status.l_max, l_status.i_sum+r_status.l_max)
        r_max = max(r_status.r_max, r_status.i_sum+l_status.r_max)
        i_sum = l_status.i_sum+r_status.i_sum
        m_max = max([l_status.m_max, r_status.m_max, l_status.r_max+r_status.l_max])
        return Status(l_max, r_max, m_max, i_sum)
    
    def recursive(self, nums, lo, hi):
        if lo==hi-1:
            return Status(nums[lo], nums[lo], nums[lo], nums[lo])
        mid = (lo+hi)//2
        l_status = self.recursive(nums, lo, mid)
        r_status = self.recursive(nums, mid, hi)
        status = self.pushUp(l_status, r_status)
        return status

    def maxSubArray(self, nums: List[int]) -> int:
        return self.recursive(nums, 0, len(nums)).m_max

因为递归调用的overhead,在LeetCode原题数据量为 1 0 5 10^5 105的情况下,实测耗时与分治算法耗时相近。


http://www.kler.cn/a/430510.html

相关文章:

  • python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
  • win11永久修改pdf默认打开方式
  • 柒拾捌- 如何通过数据影响决策(六)- 放大再放大
  • 119.【C语言】数据结构之快速排序(调用库函数)
  • SpringCloudAlibaba实战入门之路由网关Gateway初体验(十一)
  • vue之axios基本使用
  • 基于 LlamaFactory 微调大模型的实体识别的评估实现
  • docker离线安装及部署各类中间件(x86系统架构)
  • fastboot
  • centos7 离线安装7z
  • 方案解读:46页数字化企业制造运营管理(MOM)系统运营实践方案
  • 【人工智能】通过ChatGPT、Claude与通义千问 API 实现智能语料知识图谱的自动化构建(详细教程)
  • 关于SpringBoot项目创建后构建总是失败的问题
  • SQL Server中SELECT (Transact-SQL)语法定义和解释
  • GAMES101:现代计算机图形学-笔记-10
  • MySQL触发器的使用详解
  • 【漫话机器学习系列】003.Agglomerative聚类
  • 分布式 Paxos算法 总结
  • 信息化时代的安全挑战与密评的重要性
  • 【Linux】软硬连接 | 静动态库
  • onnx文件转pytorch pt模型文件
  • 【Spark】Spark Join类型及Join实现方式
  • 使用 electron 把 vue 项目打包成客户端
  • liunx docker 部署 nacos seata sentinel
  • TCP/IP协议配置与网络实用命令
  • uniapp 弹出软键盘后打开二级页面,解决其UI布局变动