leetcode刷題筆記五十三 最大子序和
阿新 • • 發佈:2020-07-10
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); //更新父節點 } }