<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          區(qū)間算法題用線段樹可以秒解?

          共 6807字,需瀏覽 14分鐘

           ·

          2021-12-24 20:13


          背景

          給一個(gè)兩個(gè)數(shù)組,其中一個(gè)數(shù)組是 A [1,2,3,4],另外一個(gè)數(shù)組是 B [5,6,7,8]。讓你求兩個(gè)數(shù)組合并后的大數(shù)組的:

          • 最大值
          • 最小值
          • 總和

          這題是不是很簡單?我們直接可以很輕松地在 的時(shí)間解決,其中 m 和 n 分別為數(shù)組 A 和 B 的大小。

          那如果我可以「修改」 A 和 B 的某些值,并且我要求「很多次」最大值,最小值和總和呢?

          樸素的思路是原地修改數(shù)組,然后 的時(shí)間重新計(jì)算。顯然這并沒有利用之前計(jì)算好的結(jié)果,效率是不高的。那有沒有效率更高的做法?

          有!線段樹就可以解決。

          線段樹是什么?

          線段樹本質(zhì)上就是一棵樹。更準(zhǔn)確地說,它是一顆二叉樹,而且它是一顆平衡二叉樹。關(guān)于為什么是平衡二叉樹,我們后面會(huì)講,這里大家先有這樣一個(gè)認(rèn)識(shí)。

          雖然是一棵二叉樹,但是線段樹我們通常使用數(shù)組來模擬樹結(jié)構(gòu),而不是傳統(tǒng)的定義 TreeNode 。

          一方面是因?yàn)閷?shí)現(xiàn)起來容易,另外一方面是因?yàn)榫€段樹其實(shí)是一顆完全二叉樹,因此使用數(shù)組直接模擬會(huì)很高效。這里的原因我已經(jīng)在之前寫的堆專題中的二叉堆實(shí)現(xiàn)的時(shí)候中講過了,大家可以在我的公眾號(hào)《力扣加加》回復(fù)「堆」獲取。

          線段樹解決什么問題?

          正如它的名字,線段樹和線段(區(qū)間)有關(guān)。線段樹的每一個(gè)樹節(jié)點(diǎn)其實(shí)都存儲(chǔ)了一個(gè)「區(qū)間(段)的信息」。然后這些區(qū)間的信息如果「滿足一定的性質(zhì)」就可以用線段樹來提高性能。

          那:

          1. 究竟是什么樣的性質(zhì)?
          2. 如何提高的性能呢?

          究竟是什么樣的性質(zhì)?

          比如前面我們提到的最大值,最小值以及求和就滿足這個(gè)「一定性質(zhì)」。即我可以根據(jù)若干個(gè)(這里是兩個(gè))子集推導(dǎo)出子集的并集的某一指標(biāo)。

          以上面的例子來說,我們可以將數(shù)組 A 和 數(shù)組 B 看成兩個(gè)集合。那么:集合 A 的最大值和集合 B 的最大值已知,我們可以直接通過 max(Amax, Bmax) 求得集合 A 與集合 B 的并集的最大值。其中 Amax 和 Bmax 分別為集合 A 和集合 B 的最大值。最小值和總和也是一樣的,不再贅述。因此如果統(tǒng)計(jì)信息滿足這種性質(zhì),我們就可以可以使用線段樹。但是要不要使用,還是要看用了線段樹后,是否能提高性能。

          如何提高的性能呢?

          關(guān)于提高性能,我先賣一個(gè)關(guān)子,等后面講完實(shí)現(xiàn)的時(shí)候,我們?cè)倭摹?/p>

          實(shí)現(xiàn)

          以文章開頭的求和為例。

          我們可以將區(qū)間 A 和 區(qū)間 B 分別作為一個(gè)樹的左右節(jié)點(diǎn),并將 A 的區(qū)間和與 B 的區(qū)間和分別存儲(chǔ)到左右子節(jié)點(diǎn)中。

          接下來,將 A 的區(qū)間分為左右兩部分,同理 B 也分為左右兩部分。不斷執(zhí)行此過程直到無法繼續(xù)分。

          總結(jié)一下就是將區(qū)間不斷一分為二,并將區(qū)間信息分別存儲(chǔ)到左右節(jié)點(diǎn)。如果是求和,那么區(qū)間信息就是區(qū)間的和。這個(gè)時(shí)候的線段樹大概是這樣的:

          ?

          藍(lán)色字體表示的區(qū)間和。

          ?

          注意,這棵樹的所有葉子節(jié)點(diǎn)一共有 n 個(gè)(n 為原數(shù)組長度),并且每一個(gè)都對(duì)應(yīng)到原數(shù)組某一個(gè)值。

          體現(xiàn)到代碼上也很容易。直接使用「后續(xù)遍歷」即可解決。這是因?yàn)椋覀冃枰雷笥夜?jié)點(diǎn)的統(tǒng)計(jì)信息,才能計(jì)算出當(dāng)前節(jié)點(diǎn)的統(tǒng)計(jì)信息。

          ?

          不熟悉后序遍歷的可以看下我之前的樹專題,公眾號(hào)力扣加加回復(fù)樹即可獲取

          ?

          和二叉堆的表示方式一樣,我們可以用數(shù)組表示樹,用 2 * i + 12 * 1 + 2 來表示左右節(jié)點(diǎn)的索引,其中 i 為當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)在 tree 上的索引。

          ?

          tree 是用來構(gòu)建線段樹的數(shù)組,和二叉堆類似。只不過 tree[i] 目前存的是區(qū)間信息罷了。

          ?

          上面我描述建樹的時(shí)候有明顯的遞歸性,因此我們可以遞歸的建樹。具體來說,可以定義一個(gè) build(tree_index, l, r) 方法 來建樹。其中 l 和 r 就是對(duì)應(yīng)區(qū)間的左右端點(diǎn),這樣 l 和 r 就可以唯一確定一個(gè)區(qū)間。tree_index 其實(shí)是用來標(biāo)記當(dāng)前的區(qū)間信息應(yīng)該被更新到 tree 數(shù)組的哪個(gè)位置。

          我們?cè)?tree 上存儲(chǔ)區(qū)間信息,那么最終就可以用 tree[tree_index] = .... 來更新區(qū)間信息啦。

          核心代碼:

          def?build(self,?tree_index:int,?l:int,?r:int):
          ????'''
          ????遞歸創(chuàng)建線段樹
          ????tree_index?:?線段樹節(jié)點(diǎn)在數(shù)組中位置
          ????l,?r?:?該節(jié)點(diǎn)表示的區(qū)間的左,右邊界
          ????'''

          ????if?l?==?r:
          ????????self.tree[tree_index]?=?self.data[l]
          ????????return
          ????mid?=?(l+r)?//?2?#?區(qū)間中點(diǎn),對(duì)應(yīng)左孩子區(qū)間結(jié)束,右孩子區(qū)間開頭
          ????left,?right?=?2?*?tree_index?+?1,?2?*?tree_index?+?2?#?tree_index?的左右子樹索引
          ????self.build(left,?l,?mid)
          ????self.build(right,?mid+1,?r)
          ????#?典型的后序遍歷
          ????#?區(qū)間和使用加法即可,如果不是區(qū)間和要改下面這行代碼
          ????self.tree[tree_index]?=?self.tree[left]?+?self.tree[right]

          上面代碼的數(shù)組 self.tree[i] 其實(shí)就是用來存類似上圖中藍(lán)色字體的區(qū)間和。「每一個(gè)區(qū)間都在 tree 上存有它一個(gè)位置,存它的區(qū)間和」

          「復(fù)雜度分析」

          • 時(shí)間復(fù)雜度:由遞推關(guān)系式 T(n) = 2*T(n/2) + 1,因此時(shí)間復(fù)雜度為
          ?

          不知道怎么得出的 ? 可以看下我的《算法通關(guān)之路》的第一章內(nèi)容。https://leetcode-solution.cn/book-intro

          ?
          • 空間復(fù)雜度:tree 的大小和 n 同階,因此空間復(fù)雜度為

          終于把樹建好了,但是知道現(xiàn)在一點(diǎn)都沒有高效起來。我們要做的是高效處理「頻繁更新情況下的區(qū)間查詢」

          那基于這種線段樹的方法,如果更新和查詢區(qū)間信息如何做呢?

          區(qū)間查詢

          先回答簡單的問題「區(qū)間查詢」原理是什么。

          如果查詢一個(gè)區(qū)間的信息。這里也是使用后序遍歷就 ok 了。比如我要找一個(gè)區(qū)間 [l,r] 的區(qū)間和。

          那么如果當(dāng)前左節(jié)點(diǎn):

          • 完整地落在 [l,r] 內(nèi)。比如 [2,3] 完整地落在 [1,4] 內(nèi)。我們直接將 tree 中左節(jié)點(diǎn)對(duì)于的區(qū)間和取出來備用,不妨極為 lsum。
          • 部分落在 [l,r] 內(nèi)。比如 [1,3] 部分落在 [2,4]。這個(gè)時(shí)候我們繼續(xù)遞歸,直到完整地落在區(qū)間內(nèi)(上面的那種情況),這個(gè)時(shí)候我們直接將 tree 中左節(jié)點(diǎn)對(duì)于的區(qū)間和取出來備用
          • 將前面所有取出來備用的值加起來就是答案

          右節(jié)點(diǎn)的處理也是一樣的,不再贅述。

          「復(fù)雜度分析」

          • 時(shí)間復(fù)雜度:查詢不需要在每個(gè)時(shí)刻都處理兩個(gè)葉子節(jié)點(diǎn),實(shí)際上處理的次數(shù)大致和樹的高度一致。而樹是平衡的,因此復(fù)雜度為

          或者由遞推關(guān)系式 T(n) = T(n/2) + 1,因此時(shí)間復(fù)雜度為

          ?

          不知道怎么得出的 ? 可以看下我的《算法通關(guān)之路》的第一章內(nèi)容。https://leetcode-solution.cn/book-intro

          ?

          大家可以結(jié)合后面的代碼理解這個(gè)復(fù)雜度。

          區(qū)間修改

          那么如果我修改了 A[1] 為 1 呢?

          如果不修改 tree,那么顯然查詢的區(qū)間只要包含了 A[1] 就一定是錯(cuò)的,比如查詢區(qū)間 [1,3] 的和 就會(huì)得到錯(cuò)誤的答案。因此我們要在修改了 A[1] 的時(shí)候同時(shí)去修改 tree。

          問題在于「我們要修改哪些 tree 的值,修改為多少呢?」

          首先回答第一個(gè)問題,修改哪些 tree 的值呢?

          我們知道,線段樹的葉子節(jié)點(diǎn)都是原數(shù)組上的值,也是說,線段樹的 n 個(gè)葉子節(jié)點(diǎn)對(duì)應(yīng)的就是原數(shù)組。因此我們首先要「找到我們修改的位置對(duì)應(yīng)的那個(gè)葉子節(jié)點(diǎn),將其值修改掉?!?/strong>

          這就完了么?

          沒有完。實(shí)際上,我們修改的葉子節(jié)點(diǎn)的所有父節(jié)點(diǎn)以及祖父節(jié)點(diǎn)(如果有的話)都需要改。也就是說我們需要「從這個(gè)葉子節(jié)點(diǎn)不斷冒泡到根節(jié)點(diǎn),并修改沿途的區(qū)間信息」

          ?

          這個(gè)過程和瀏覽器的事件模型是類似的

          ?

          接下來回答最后一個(gè)問題,具體修改為多少?

          對(duì)于求和,我們需要首先將葉子節(jié)點(diǎn)改為修改后的值,另外所有葉子節(jié)點(diǎn)到根節(jié)點(diǎn)路徑上的點(diǎn)的區(qū)間和都加上 delta,其中 delta 就是改變前后的差值。

          ?

          求最大最小值如何更新?大家自己思考一下。

          ?

          修改哪些節(jié)點(diǎn),修改為多少的問題都解決了,那么代碼實(shí)現(xiàn)就容易了。

          「復(fù)雜度分析」

          • 時(shí)間復(fù)雜度:修改不需要在每個(gè)時(shí)刻都處理兩個(gè)葉子節(jié)點(diǎn),實(shí)際上處理的次數(shù)大致和樹的高度一致。而樹是平衡的,因此復(fù)雜度為

          或者由遞推關(guān)系式 T(n) = T(n/2) + 1,因此時(shí)間復(fù)雜度為

          ?

          不知道怎么得出的 ? 可以看下我的《算法通關(guān)之路》的第一章內(nèi)容。https://leetcode-solution.cn/book-intro

          ?

          大家可以結(jié)合后面的代碼理解這個(gè)復(fù)雜度。

          代碼

          線段樹代碼已經(jīng)放在刷題插件上了,公眾號(hào)《力扣加加》回復(fù)插件即可獲取。


          class?SegmentTree:
          ????def?__init__(self,?data:List[int]):
          ????????'''
          ????????data:?傳入的數(shù)組
          ????????'''

          ????????self.data?=?data
          ????????self.n?=?len(data)
          ????????#??申請(qǐng)?4?倍?data?長度的空間來存線段樹節(jié)點(diǎn)
          ????????self.tree?=?[None]?*?(4?*?self.n)?#?索引?i?的左孩子索引為?2i+1,右孩子為?2i+2
          ????????if?self.n:
          ????????????self.build(0,?0,?self.n-1)
          ????#?本質(zhì)就是一個(gè)自底向上的更新過程
          ????#?因此可以使用后序遍歷,即在函數(shù)返回的時(shí)候更新父節(jié)點(diǎn)。
          ????def?update(self,?tree_index,?l,?r,?index):
          ????????'''
          ????????tree_index:?某個(gè)根節(jié)點(diǎn)索引
          ????????l,?r?:?此根節(jié)點(diǎn)代表區(qū)間的左右邊界
          ????????index?:?更新的值的索引
          ????????'''

          ????????if?l?==?r==index?:
          ????????????self.tree[tree_index]?=?self.data[index]
          ????????????return
          ????????mid?=?(l+r)//2
          ????????left,?right?=?2?*?tree_index?+?1,?2?*?tree_index?+?2
          ????????if?index?>?mid:
          ????????????#?要更新的區(qū)間在右子樹
          ????????????self.update(right,?mid+1,?r,?index)
          ????????else:
          ????????????#?要更新的區(qū)間在左子樹?index<=mid
          ????????????self.update(left,?l,?mid,?index)
          ????????#?查詢區(qū)間一部分在左子樹一部分在右子樹
          ????????#?區(qū)間和使用加法即可,如果不是區(qū)間和要改下面這行代碼
          ????????self.tree[tree_index]?=?self.tree[left]?+?self.tree[right]

          ????def?updateSum(self,index:int,value:int):
          ????????self.data[index]?=?value
          ????????self.update(0,?0,?self.n-1,?index)
          ????def?query(self,?tree_index:int,?l:int,?r:int,?ql:int,?qr:int)?->?int:
          ????????'''
          ????????遞歸查詢區(qū)間?[ql,..,qr]?的值
          ????????tree_index?:?某個(gè)根節(jié)點(diǎn)的索引
          ????????l,?r?:?該節(jié)點(diǎn)表示的區(qū)間的左右邊界
          ????????ql,?qr:?待查詢區(qū)間的左右邊界
          ????????'''

          ????????if?l?==?ql?and?r?==?qr:
          ????????????return?self.tree[tree_index]

          ????????#?區(qū)間中點(diǎn),對(duì)應(yīng)左孩子區(qū)間結(jié)束,右孩子區(qū)間開頭
          ????????mid?=?(l+r)?//?2
          ????????left,?right?=?tree_index?*?2?+?1,?tree_index?*?2?+?2
          ????????if?qr?<=?mid:
          ????????????#?查詢區(qū)間全在左子樹
          ????????????return?self.query(left,?l,?mid,?ql,?qr)
          ????????elif?ql?>?mid:
          ????????????#?查詢區(qū)間全在右子樹
          ????????????return?self.query(right,?mid+1,?r,?ql,?qr)

          ????????#?查詢區(qū)間一部分在左子樹一部分在右子樹
          ????????#?區(qū)間和使用加法即可,如果不是區(qū)間和要改下面這行代碼
          ????????return?self.query(left,?l,?mid,?ql,?mid)?+?self.query(right,?mid+1,?r,?mid+1,?qr)

          ????def?querySum(self,?ql:int,?qr:int)?->?int:
          ????????'''
          ????????返回區(qū)間?[ql,..,qr]?的和
          ????????'''

          ????????return?self.query(0,?0,?self.n-1,?ql,?qr)

          ????def?build(self,?tree_index:int,?l:int,?r:int):
          ????????'''
          ????????遞歸創(chuàng)建線段樹
          ????????tree_index?:?線段樹節(jié)點(diǎn)在數(shù)組中位置
          ????????l,?r?:?該節(jié)點(diǎn)表示的區(qū)間的左,右邊界
          ????????'''

          ????????if?l?==?r:
          ????????????self.tree[tree_index]?=?self.data[l]
          ????????????return
          ????????mid?=?(l+r)?//?2?#?區(qū)間中點(diǎn),對(duì)應(yīng)左孩子區(qū)間結(jié)束,右孩子區(qū)間開頭
          ????????left,?right?=?2?*?tree_index?+?1,?2?*?tree_index?+?2?#?tree_index?的左右子樹索引
          ????????self.build(left,?l,?mid)
          ????????self.build(right,?mid+1,?r)
          ????????#?區(qū)間和使用加法即可,如果不是區(qū)間和要改下面這行代碼
          ????????self.tree[tree_index]?=?self.tree[left]?+?self.tree[right]

          相關(guān)專題

          大家可以看下我之前寫的堆的專題的二叉堆實(shí)現(xiàn)。然后對(duì)比學(xué)習(xí),順便還學(xué)了堆,豈不美哉?

          • 樹狀數(shù)組

          樹狀數(shù)組和線段樹類似,難度比線段樹稍微高一點(diǎn)點(diǎn)。有機(jī)會(huì)給大家寫一篇樹狀數(shù)組的文章。

          • immutablejs

          前端的小伙伴應(yīng)該知道 immutable 吧?而 immutablejs 就是非常有名的實(shí)現(xiàn) immutable 的工具庫。西法之前寫過一篇 immutable 原理解析文章,感興趣的可以看下 https://lucifer.ren/blog/2020/06/13/immutable-js/

          回答前面的問題

          為啥是平衡二叉樹?

          前面的時(shí)間復(fù)雜度其實(shí)也是基于樹是平衡二叉樹這一前提。那么線段樹一定是平衡二叉樹么?是的。這是因?yàn)榫€段樹是完全二叉樹,而完全二叉樹是平衡的。

          當(dāng)然還有另外一個(gè)前提,那就是樹的總的節(jié)點(diǎn)數(shù)和原數(shù)組長度同階,也就是 n 的量級(jí)。關(guān)于為啥是同階的,也容易計(jì)算,只需要根據(jù)遞歸公式即可得出。

          為啥線段樹能提高性能?

          只要你理解了我「實(shí)現(xiàn)部分」的時(shí)間復(fù)雜度,那么就不難明白這個(gè)問題。因?yàn)樾薷暮筒樵兊臅r(shí)間復(fù)雜度都是 ,而不使用線段樹的暴力做法查詢的復(fù)雜度高達(dá) 。相應(yīng)的代價(jià)就是建樹的 的空間,因此線段樹也是一種典型的空間換時(shí)間算法。

          最后點(diǎn)一下題。區(qū)間算法題是否可以用線段樹秒解?這其實(shí)文章中已經(jīng)回答過了,其取決于是否滿足兩點(diǎn):

          1. 是否可以根據(jù)若干個(gè)(這里是兩個(gè))子集推導(dǎo)出子集的并集的某一指標(biāo)。
          2. 是否能提高性能(相比于樸素的暴力解法)。通常面臨「頻繁修改」的場景都可以考慮使用線段樹「優(yōu)化修改后的查詢時(shí)間消耗」。

          我的書 「《算法通關(guān)之路》」 已經(jīng)出版了,想要突破算法面試的朋友不要錯(cuò)過,京東淘寶當(dāng)當(dāng)亞馬遜等均有出售,電子版也有哦~

          愛心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的在看是我創(chuàng)作的動(dòng)力。

          2.關(guān)注公眾號(hào)力扣加加,獲取更多算法硬核文章!加個(gè)星標(biāo),不錯(cuò)過每一條成長的機(jī)會(huì)。

          3.如果你覺得本文的內(nèi)容對(duì)你有幫助,就幫我轉(zhuǎn)發(fā)一下吧。

          • 后臺(tái)回復(fù):「電子書」,獲取我精心制作的算法刷題電子書(20+萬字)
          • 后臺(tái)回復(fù):「背包」,自動(dòng)獲取《背包九講》pdf
          • 后臺(tái)回復(fù):「腦圖」,自動(dòng)獲取我制作的算法腦圖總結(jié)
          • 后臺(tái)回復(fù):「刷題插件」,自動(dòng)獲取上萬人都在用的力扣刷題插件

          另外你還可以回復(fù)具體的算法專題獲取相應(yīng)的文章,比如 「二分」,「堆」「樹」,「鏈表」等等

          微信更新了推送規(guī)則,優(yōu)先推送有互動(dòng)的公眾號(hào),為了不錯(cuò)過更多優(yōu)質(zhì)文章,大家不妨點(diǎn)個(gè)贊!




          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩黄色一级片 | 亚洲自拍在线观看 | 99re10 | 亚洲日韩毛片 | 黄色免费在线观看电影 |