學習筆記——數列分塊
一、沒什麼用的前言
首先,讓大家來看一張表格
POJ3468 | 複雜度 | 時間 | 記憶體 | 程式碼 | 優劣 |
---|---|---|---|---|---|
樹狀陣列 | $O((N+Q)logN)$ | $1.0s$ | $3MB$ | $850B$ | 效率高、程式碼短 不易擴充套件、不太直觀 |
線段樹 | $O((N+Q)logN)$ | $1.5s$ | $7MB$ | $1700B$ | 效率較高、擴充套件性好 程式碼較長、直觀性一般 |
分塊 | $O((N+Q)\sqrt{N})$ | $1.9s$ | $1.5MB$ | $1500B$ | 通用、直觀 效率較低、碼長一般 |
樸素 | $O((N+Q) \times N)$ | $TLE$ | $1MB$ | $500B$ | 效率低 通用 |
--$from$ 《演算法競賽進階指南》
看到這裡,$OIer$們作何感想?(我還學什麼分塊啊,學樹狀陣列和線段樹不香嗎?),所以本篇部落格到此結束。
好了好了,先收一下浮躁的內心,讓我們換個角度再思考一遍:
我們學會了樹狀陣列 (時空複雜度吊打其他資料結構) ,但那也只能解決單點修改區間查詢的問題(加個差分可以解決區間修改單點查詢),面對其他問題,我們不就束手無策了嗎?
有$OIer$就要說了:那我用線段樹不就可以了嗎?
那如果問題不具有區間可加性呢?
這時候,就輪到分塊閃亮登場了!
俗話說的好(其實只在$OI$界流行):複雜度越高的演算法,能處理的問題就越多,功能就越全面。所以儘管分塊的複雜度更高,但處理起問題來,應用的也更為廣泛。
二、分塊的概念
我們先回顧樹狀陣列和線段樹的原理:一個基於二進位制劃分與倍增,一個基於分治,它們都將序列分為幾段,再對每一段進行維護。
同理,分塊就是將序列分為幾個小塊,並採用整塊維護,區域性樸素的方法進行查詢,最後得出結論。
最優分塊公式:
$blo$(分塊的塊長)$=$ $max(1,\dfrac{n}{ \sqrt{(m \times log_2n) }})$
其中$n$為數列長度,$m$為詢問次數。
三、練習
是不是感覺很奇怪,分塊的概念就這麼點?沒錯,就這麼點,但其題目的難度也夠考驗(e xin)我們了。
不信,那就先來一下九道入門題:
兩篇不錯的題解:
T1
思路:和線段樹一樣,用一個add
陣列記錄整塊加的數,再查詢即可。
T2
思路:用另一個數組記錄從小到大排好序後的整塊內元素,再用lower_bound
(返回第一個$>=$查詢值的下標)統計個數,其他與$T1$一樣。
T3
思路:和$T2$一樣,先用另一個數組記錄從小到大排好序後的整塊內元素,再用lower_bound
找整塊內的前驅以及樸素掃描分塊多出部分的前驅。
T4
思路:和線段樹模板$1$一樣,用add
和sum
兩個陣列記錄整塊的修改操作,再查詢即可。
T5
思路:加一個標記陣列,如果該整塊內的元素都為$0$或$1$時,就標記該整塊,之後不用再修改。
T6
思路:藉助vector
進行插入操作,當一個塊的長度超過一定限度時,將整個陣列進行分塊的重構,防止因塊長過長而$TLE$
T7
思路:和線段樹模板$2$一樣,用add
和mul
兩個陣列記錄整塊的修改操作,再查詢即可。
T8
思路:加一個標記陣列(設為tag
),初始時全部賦值為$INF$。修改時對於整塊直接修改標記,對於兩端多出的部分先把整個塊賦值為標記,把tag
陣列賦值為$INF$,再修改兩端多出的部分$a[i]$的值。查詢時對整塊直接判斷標記,如果tag
為$INF$就樸素掃描該塊,對於兩端多出的部分先下推tag
再掃描即可。
T9
思路:本題為區間眾數,不具有區間可加性,只能分塊處理。
對於眾數的來源,只可能為以下三種情況:
1.來源於l
到pos[l]*blo
(pos[l]
表示$l$所屬的分塊)之間。
2.來源於pos[l]+1
到pos[r]-1
之間。
3.來源於(pos[r]-1)*blo+1
到r
之間。
我們可以用一個二維陣列f[i][j]
記錄塊$i$到塊$j$之間的眾數,並對原陣列進行離散化,用vector
記錄$a[i]$中的元素的位置,用upper_bound
減去lower_bound
即可得到區間內該元素出現次數。
怎麼樣,感受到分塊的妙處了嗎?那我們再刷幾道洛谷的分塊題吧!
P2801 教主的魔法(類似入門二)(適合分塊入門的題解)我的程式碼
P4145 上帝造題的七分鐘2 / 花神遊歷各國(類似入門五)(分塊題解(當然其他資料結構也能過,但咱們這兒是分塊練習嘛))我的程式碼
P4168 (Violet)蒲公英(類似入門九)(題解)(我的程式碼)
P5356 [Ynoi2017] 由乃打撲克(題解)(我的程式碼)
眾所周知,$YNOI$是分塊卡常專項練習題,所以咱們的卡常技巧當然是少不了的:
-
每一次修改後,對於只修改了一部分的小段,因為修改段和未修改段都還是單調的,所以可以運用歸併排序的方法$O(n)$排序,不必用$sort$花$O(nlogn)$的時間
-
查詢時,對於多出的兩個小段,我們可以用歸併排序將其合併為一個塊(原理同上),再進行二分答案
-
二分答案時,二分的邊界為每一塊的最大值的最大值,每一塊的最小值的最小值,不用從$-INF$到$INF$。
-
大優化 :如果一次二分至於得到的答案($<=mid$的數的個數)小於要求時,可以統計每一塊中最後一個小於等於$mid$的數的位置,把該塊的答案初始值設為該位置$-$該塊左起點位置$+1$,再把該塊的左起點設為該位置$+1$,可大量減少二分割槽域。
P5309 [Ynoi2011] 初始化(題解)(我的程式碼)
本題屬於分塊$+$根號分治,要對$x$的範圍進行分類討論
-
當$x>blo$時,因為要修改的區間小於$\sqrt n$,所以直接暴力修改即可
-
當$x<blo$時,我們可以以$x$的值作為塊長統計貢獻:
因為筆者不太會使用畫圖軟體,所以獻上一張醜圖將就著看吧
如上圖所示,我們可以用一個二維陣列記錄該修改對紅藍各部分的貢獻的字首字尾和
我用pre[i][j]
表示當塊長為$i$時距離塊首為$j$的位置對答案的貢獻的字首和,用suf[i][j]
表示塊長為$i$時距離塊首為$j$的位置對答案的貢獻的字尾和。
-
對於$l$、$r$在塊長為$i$時的同一塊的情況,只需要用$r$位置的貢獻的字首和減去$l-1$位置的貢獻的字首和即可
-
對於$l$、$r$在塊長為$i$時的不同塊的情況,用$l$位置的貢獻的字尾和加上$r$位置貢獻的字首和,再加上$l$和$r$之間間隔的整塊數$\times$該塊的貢獻(
pre[i][i]
或suf[i][1]
)即可