leetcode刷题笔记五十三 最大子序和
leetcode刷题笔记五十三 最大子序和
源地址:53. 最大子序和
问题描述:
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
代码补充:
//通过观察,易发现本题可以通过动态规划解题
//初始状态: maxSum(0) = nums(0)
//状态转换方程: maxSum(i) = math.max(maxSum(i-1)+nums(i), nums(i))
//为了节省空间,使用nums记忆其对应的maxSum
//时间复杂度:O(n) 空间复杂度:O(1)
object Solution {
def maxSubArray(nums: Array[Int]): Int = {
val length = nums.length
if(length == 0) return 0
for(i <- 1 until length){
nums(i) = math.max(nums(i-1)+nums(i), nums(i))
}
return nums.max
}
}
/**
分治法参考了官方题解,其中提到了线段树数据结构
官方题解:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/
将问题get(arr, left, right)的问题划分为get(arr, left, mid)
get(arr, mid+1, right) 再对两个子问题的结果进行合并
针对[l, r]区间,需要维护4个量
这四个量的计算都是基于区间位置是否位于子区间,是否跨越两区间计算
lSum表示[l,r]内以l为左端点的最大子段和
rSum表示[l,r]内以r为右端点的最大字段和
mSum表示[l,r]内的最大子段和
iSum表示[l,r]区间和
iSum = lMerv.iSum + rMerv.iSum
lSum = math.max(lMerv.lSum, lMerv.iSum+rMerv.lSum)
rSum = math.max(rMerv.rSum, rMerv.iSum+lMerv.rSum)
mSum = math.max(math.max(lMerv.mSum, rMerv.mSum), lMerv.rSum+rMerv.lSum)
时间复杂度:O(logn) ---> O(n) 空间复杂度:O(logn)
*/
object Solution {
def maxSubArray(nums: Array[Int]): Int = {
class mervTree(var iSum:Int, var lSum:Int, var rSum:Int, var mSum:Int)
def pushUp(lMerv: mervTree, rMerv: mervTree): mervTree = {
val iSum = lMerv.iSum + rMerv.iSum
val lSum = math.max(lMerv.lSum, lMerv.iSum+rMerv.lSum)
val rSum = math.max(rMerv.rSum, rMerv.iSum+lMerv.rSum)
val mSum = math.max(math.max(lMerv.mSum, rMerv.mSum), lMerv.rSum+rMerv.lSum)
return new mervTree(iSum, lSum, rSum, mSum)
}
def get(nums: Array[Int], l: Int, r: Int) : mervTree = {
if (l == r) return new mervTree(nums(l), nums(l), nums(l), nums(l))
val m = (l + r)/2
val lPush = get(nums, l, m)
val rPush = get(nums, m+1, r)
return pushUp(lPush, rPush)
}
val length = nums.length
return get(nums, 0, length-1).mSum
}
}
知识补充:
线段树:
线段树内容参考:https://www.cnblogs.com/xenny/p/9801703.html 与 https://www.cnblogs.com/jason2003/p/9676729.html
线段树概念,以本题为例,如图所示 为一种特殊二叉树
递归建树:
inline void build(int i,int l,int r){//递归建树 tree[i].l=l;tree[i].r=r; if(l==r){//如果这个节点是叶子节点 tree[i].sum=input[l]; return ; } int mid=(l+r)>>1; build(i*2,l,mid);//分别构造左子树和右子树 build(i*2+1,mid+1,r); tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//刚才我们发现的性质return ; }
线段树的查询方法:
- 如果这个区间被包含在目标区间里面,直接返回这个区间的值
- 如果这个区间与目标区间毫不相干, 返回0
- 如果这个区间的左儿子和目标区间有交集,搜索左儿子
- 如果这个区间的右儿子和目标区间有交集,搜索右儿子
inline int search(int i,int l,int r){ if(tree[i].l>=l && tree[i].r<=r)//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值 return tree[i].sum; if(tree[i].r<l || tree[i].l>r) return 0;//如果这个区间和目标区间毫不相干,返回0 int s=0; if(tree[i*2].r>=l) s+=search(i*2,l,r);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子 if(tree[i*2+1].l<=r) s+=search(i*2+1,l,r);//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子 return s; }
线段树的区间更新与lazytag
lazytag
线段树在进行区间更新的时候,为了提高更新的效率,所以每次更新只更新到更新区间完全覆盖线段树结点区间为止,这样就会导致被更新结点的子孙结点的区间得不到需要更新的信息,所以在被更新结点上打上一个标记,称为lazytag,等到下次访问这个结点的子结点时再将这个标记传递给子结点,所以也可以叫延迟标记。递归更新的过程,更新到结点区间为需要更新的区间的真子集不再往下更新,下次若是遇到需要用这下面的结点的信息,再去更新这些结点,复杂度为O(logn)。void Pushdown(int k){ //更新子树的lazy值,这里是RMQ的函数,要实现区间和等则需要修改函数内容 if(lazy[k]){ //如果有lazy标记 lazy[k<<1] += lazy[k]; //更新左子树的lazy值 lazy[k<<1|1] += lazy[k]; //更新右子树的lazy值 t[k<<1] += lazy[k]; //左子树的最值加上lazy值 t[k<<1|1] += lazy[k]; //右子树的最值加上lazy值 lazy[k] = 0; //lazy值归0 } } //递归更新区间 updata(L,R,v,1,n,1); void updata(int L,int R,int v,int l,int r,int k){ //[L,R]即为要更新的区间,l,r为结点区间,k为结点下标 if(L <= l && r <= R){ //如果当前结点的区间真包含于要更新的区间内 lazy[k] += v; //懒惰标记 t[k] += v; //最大值加上v之后,此区间的最大值也肯定是加v } else{ Pushdown(k); //重难点,查询lazy标记,更新子树 int m = l + ((r-l)>>1); if(L <= m) //如果左子树和需要更新的区间交集非空 update(L,R,v,l,m,k<<1); if(m < R) //如果右子树和需要更新的区间交集非空 update(L,R,v,m+1,r,k<<1|1); Pushup(k); //更新父节点 } }