使用Extjs和PHP快速構建Web應用系統(三)
線段樹專題
一、線段樹基礎
1. 線段樹簡介
線段樹是演算法競賽中常用的用來維護區間資訊的資料結構。
線段樹可以在很小的時間複雜度內實現 單點修改、區間修改、區間查詢(即區間求和,求區間 \(max\) ,求區間 \(min\) ,區間 \(gcd\) 等)操作。
但是,線段樹所維護的資訊,需要滿足區間加法。
區間加法:如果一個區間 \([l,r]\)(線段樹中一個點表示一個區間)滿足區間加法的意思是一個區間 \([l,r]\) 的線段樹維護的資訊(即區間最大值,區間最小值,區間和,區間 \(gcd\) 等),可以由兩個區間 \([l,mid]\)
2. 線段樹的基本概念
線段樹,是一種基於分治思想的二叉搜尋樹。它支援的所有操作都可以 \(O(logn)\) 的時間複雜度完成。但是線段樹有個很大的特點:線段樹的題目可以調上一整天
線段樹的 基本特點:
- 線段樹的每一個節點表示一個區間
- 線段樹有唯一根,這個根表示的所有會被線段樹統計的總區間,一般情況下,根表示的區間就是 \([1,n]\)
- 線段樹的葉子節點表示的區間為 \([x,x]\) ,且長度為 \(1\)
- 線段樹中如果一個節點表示的區間是 \([l,r]\) ,且這個點不為葉子節點,即 \(l≠r\),那麼這個節點的左子樹的根表示的區間就是 \([l,mid]\)
3.線段樹的儲存方式
直接採用堆儲存方式,即一顆線段樹的跟的編號是 \(1\) ,設一個不為根的節點編號 \(x\) ,則這個點的父節點是 \(⌊\frac{x}{2}⌋\) ,他的兩個子節點的編號分別是 \(2×x\) 和 \(2×x+1\) 。為了線段樹的節點不超過儲存範圍,一般線段樹都要開 \(4×n\) 的空間,即區間總長度的 \(4\) 倍。
因為一顆線段樹最多是一顆滿二叉樹,而滿二叉樹的最後一層是\(n\) 個點,前面的點數是 \(n−1\) ,所以一共要 \(2×n−1\)
這裡就是一個例子,最後一層是多出來的,而最後一層節點最多 \(2×n\) 個節點,最壞情況下就最右邊兩個節點,最右下角的一個節點的編號是 \(2×n−1+2×n=4×n−1\) ,所以線段樹一般開 \(4×n\) 。
4.建立線段樹
思路
我們遞迴遍歷初始區間,把遍歷到的所有節點表示的區間記錄下來,如果這個節點不是葉子節點(即區間長度大於\(1\)),那麼就分別遍歷左子樹和右子樹,否則就是葉子節點,不僅要把表示的區間記錄下來,還要把線段樹維護的資訊也記錄下來,維護的資訊在葉子節點上基本上就是這個數本身。
時間複雜度 \(O(logn)\)
程式碼
struct Node {
int l,r;
LL sum; //這裡可以維護任何滿足區間加法的資訊,這裡就用區間求和了
}tr[N << 2]; //要開四倍空間
void pushup (int u) { //這裡只有區間和,區間和就是由一個點的左右子節點的和相加
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build (int u,int l,int r) { //當前正在下標為u的點,這個點表示的區間是[l,r]
if (l == r) {
tr[u] = {l,r,a[l]};
return ;
}
tr[u] = {l,r}; //記得儲存當前點表示的區間,否則你會調上一整天
int mid = l + r >> 1;
build (u << 1,l,mid),build (u << 1 | 1,mid + 1,r); //u << 1就是u * 2,u << 1 | 1就是u * 2 + 1
pushup (u); //pushup函式的意思是把某一個節點的資訊有他的子節點算出來
}
5.單點修改
思路
我們通過二分查詢的形式找到要修改的點,然後把找的過程上的鏈都修改一下。
時間複雜度 \(O(logn)\)
程式碼
void modify (int u,int x,int v) { //當前這個點是下標為u的點,要把第x個數修改成d
if (tr[u].l == tr[u].r) {
tr[u].sum = v; //如果當前區間只有一個數,那麼這個數一定是要修改的。
return ;
}
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify (u << 1 , x , v); //如果在左邊就遞迴修改左半邊區間
else modify (u << 1 | 1 , x , v); //如果在右邊就遞迴修改右半邊區間
pushup (u) //記得更新資訊
}
6.區間修改
思路1
線段樹的區間修改大體分為兩步
- 找到區間中全部都是要修改的點的線段樹中的區間
- 修改這一段區間的所有點
我們先解決第 \(1\) 步:
我們從根節點(根節點一定包含所有點,既包含修改區間)出發,一直往下走,直到當前區間中的元素全部都是被修改元素。
當左區間包含整個被修改區間時,我們就遞迴到左區間;
當右區間包含整個被修改區間時,我們就遞迴到右區間;
否則區間的樣子就如下圖所示:
此時該怎麼辦呢?
不過,通過思考,我們可以發現,被修改區間中的元素間,兩兩之間都不會產生影響。
所以,我們可以把被修改區間分解成兩段,使得其中的一段完全在左區間,另一端完全在右區間。
很明顯,直接在 \(mid\) 的位置將該區間切開是最好的。如下圖所示:
如果當前區間包含了修改區間的話就直接修改,把區間裡的每一個數遍歷一遍。
時間複雜度 \(O(nlogn)\)
程式碼
void modify (int u,int l,int r,int d) { //當前遍歷到的點下標是u,要將區間[l,r]增加d
if (tr[u].l == tr[u].r) { //葉子節點
tr[u].sum += d;
return ;
}
int mid = tr[u].l + tr[u].r >> 1; //注意是tr[u].l和tr[u].r
if (l <= mid) modify(u << 1,l,r,d); //左邊有修改區間,就遞迴左半邊
if (r >= mid + 1) modify(u << 1 | 1,l,r,d); //右邊有修改區間,就遞迴右半邊
pushup (u); //要記得要把這個點的資訊更新一下
}
思路2
欸,你不是說線段樹所有操作都是 \(O(logn)\) 的嗎?現在怎麼來了一個 \(O(n)\) 的?
這不就是思路 \(2\) 啊。。。
為提高線段樹的修改效率,就要引入一個好東西:懶標記
懶標記就是我們在做修改操作時能用來偷懶的標記
來自封禁的一個故事:
\(A\) 有兩個兒子,一個是 \(B\),一個是 \(C\)。
有一天 \(A\) 要建一個新房子,沒錢。剛好過年嘛,有人要給 \(B\) 和 \(C\) 紅包,兩個紅包的錢數相同都是 \(1\) 元,然而因為 \(A\) 是父親所以紅包肯定是先塞給 \(A\) 咯~
理論上來講 \(A\) 應該把兩個紅包分別給 \(B\) 和 \(C\),但是……缺錢嘛,\(A\) 就把紅包偷偷收到自己口袋裡了。
\(A\) 高興地說:「我現在有 \(2\) 份紅包了!我又多了 \(2×1=2\) 元了!哈哈哈~」
但是 \(A\) 知道,如果他不把紅包給 \(B\) 和 \(C\),那 \(B\) 和 \(C\) 肯定會不爽然後導致家庭矛盾最後崩潰,所以 \(A\) 對兒子 \(B\) 和 \(C\) 說:「我欠你們每人 \(1\) 份 \(1\) 元的紅包,下次有新紅包給過來的時候再給你們!這裡我先做下記錄……嗯……我欠你們各 \(1\) 元……」
兒子 \(B\) 、\(C\) 有點惱怒:「可是如果有同學問起我們我們收到了多少紅包咋辦?你把我們的紅包都收了,我們還怎麼裝?」
父親 \(A\) 趕忙說:「有同學問起來我就會給你們的!我欠條都寫好了不會不算話的!」
至於懶標記:
如果父親 \(A\) 把錢給 \(B\) 和 \(C\) ,接著。。。 \(A\) 就沒錢了。沒錯!懶標記就是故事中的記錄父親 \(A\) 沒給的錢賬本!
懶標記就是區間修改時偷懶用的,所以叫懶標記。
他主要解決了一個問題:如修改區間包含當前區間,就在這個節點上打個標記,表示這個點在以後的程式碼中要修改,現在先不修改。
懶標記的具體意思:我覺得 \(y\) 總的懶標記意思比較好:如果一個點上打的一個懶標記,那麼表示這個點的所有子節點都要變化這個懶標記(可以是加減乘除,\(max\),\(min\),\(gcd\)等等),注意!懶標記沒有包含當前點!
具體見圖:
我們給區間 \([1,2]\) 加上 \(DATA=1e18\) (圖左上角)
因為第一個節點包含了所有修改區間,所以我們把加的數掛在 第一個節點 上,區間修改就完成了。
當然,還有一個細節問題,就是如果一個點有懶標記,要先把他的懶標記下傳,就是把懶標記給他的兩個兒子,這就是我們要實現的 \(pushdown\) 函式。下面幾張圖片就展示了懶標記下傳的過程。
程式碼2
void pushdown (int u) { //下傳標記函式
auto &root = tr[u],&left = tr[u << 1],&right = tr[u << 1 | 1]; //加了引用符號只要修改這個變數就會修改他被賦值的變數
if (root.tag) { //有懶標記才下傳
left.tag += root.tag,left.sum += (LL)(left.r - left.l + 1) * root.tag; //這裡的子節點要加上懶標記,因為這裡懶標記的意思是不包括當前節點
right.tag += root.tag,right.sum += (LL)(right.r - right.l + 1) * root.tag; //同理
root.tag = 0; //懶標記記得清零
}
}
void modify (int u,int l,int r,int d) { //當前遍歷到的點下標是u,要將區間[l,r]增加d
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
tr[u].tag += d; //這裡我才用了y總的懶標記的意思,一個點所有的子節點都加上d,而前一行時加上根節點的數,因為不包括根節點。
return ;
}
pushdown (u); //一定要分裂,只要記牢在遞迴左右區間之前,就要分裂
int mid = tr[u].l + tr[u].r >> 1; //注意時tr[u].l和tr[u].r
if (l <= mid) modify (u << 1,l,r,d); //左邊有修改區間,就遞迴左半邊
if (r >= mid + 1) modify (u << 1 | 1,l,r,d); //右邊有修改區間,就遞迴右半邊
pushup (u); //要記得要把這個點的資訊更新一下
}
7.區間查詢
思路
區間修改類似區間修改,只不過變為查詢了。
程式碼
LL query(int u,int l,int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
pushdown (u); //在遞迴之前一定要分裂
int mid = l + r >> 1;
LL sum = 0;
if (l <= mid) sum += query_sum (u << 1,l,r); //左半邊有被查詢到的資料,就遞迴左半邊
if (r >= mid + 1) sum += query_sum (u << 1 | 1,l,r); //右半邊有被查詢到的資料,就遞迴右半邊
return sum;
}
時間複雜度 \(O(logn)\)
二、題單
(1)、單點修改
\(AcWing\) \(1275\). 最大數[單點修改區間查詢]
\(AcWing\) \(245\). 你能回答這些問題嗎[單點修改區間查詢]
\(AcWing\) \(246\). 區間最大公約數[單點修改區間查詢+差分+更相減損數]
(2)、區間修改
\(HDU\) \(1698\) \(Just\) \(a\) \(Hook\)[黃海補充+區間修改統一值+區間查詢]
\(HDU\) \(3577\) \(Fast\) \(Arrangement\) [黃海補充+線段樹+區間修改+維護最大值]
\(POJ\) \(3264\) \(Balanced\) \(Lineup\)[黃海補充+線段樹+維護最大值、最小值]
\(AcWing\) \(243\). 一個簡單的整數問題[區間增加一個值+區間查詢]
\(HDU\) \(3333\) \(Turing\) \(Tree\) [黃海補充+線段樹(樹狀陣列)+離線操作+數字去重求區間和]
\(POJ\) \(2828\) \(Buy\) \(Tickets\)[黃海補充+線段樹+二分]
(3)、掃描線法
\(AcWing\) \(1228\). 油漆面積[掃描線+線段樹,黃海補充]
(4)、線段樹優化建圖
(5)、線段樹維護樹上資訊
POJ 3321 Apple Tree [\(dfs\)序求子樹節點和]
據說線段樹還可以應用bfs序,目前還沒有找到合適的練習題,挖坑待填吧~
(6)、線段樹維護區間可合併資訊
\(POJ\) \(2777\) \(Count\) \(Color\)
(7)、線段樹維護區間不可合併資訊(暴力計算)
\(GSS4\) - \(Can\) \(you\) \(answer\) \(these\) \(queries\) \(IV\) [暴力開方]
\(P4145\) 上帝造題的七分鐘 \(2\) / 花神遊歷各國
這個其實就是上面那道題,一模一樣,輸出有點差別而已。
\(CF438D\) \(The\) \(Child\) \(and\) \(Sequence\)[暴力取模]
(8)、線段樹維護最大子段和
\(GSS1\) - \(Can\) \(you\) \(answer\) \(these\) \(queries\) \(I\) [區間最大子段和]
\(GSS3\) - \(Can\) \(you\) \(answer\) \(these\) \(queries\) \(III\)[區間最大子段和+單點修改]
\(GSS5\) - \(Can\) \(you\) \(answer\) \(these\) \(queries\) \(V\)[有範圍限制的區間最大子段和]
(9)、線段樹與思維
\(HDU\) \(5649\) \(DZY\) \(Loves\) \(Sorting\)
(10)、線段樹分裂與合併
\(P4556\) [\(Vani\)有約會]雨天的尾巴 /【模板】線段樹合併
(11)、可持久化線段樹(主席樹)
codevs 1080 (單點修改+區間查詢)
codevs 1081 (區間修改+單點查詢)
codevs 1082 (區間修改+區間查詢)
codevs 3981 (區間最大子段和)
Bzoj 3813 (區間內某個值是否出現過)
Luogu P2894 (區間連續一段空的長度)
codevs 2000 (區間最長上升子序列)
codevs 3044 (矩陣面積求並)
Hdu 1698 (區間染色+單次統計)
Poj 2777 (區間染色+批量統計)
Hdu 4419 (多色矩形面積並)
Poj 2761 (區間第K大)
Hdu 2305 (最值維護)