有一個包含\(N\)個數的序列(\(N \leq 1e6\)),給\(Q(\le 1e6)\)個操做,每一個操做是下面兩種中的一種:php
一種很暴力的想法是對每一個操做都一遍循環進行修改、求和,顯然會超時;看到區間求和很容易就能想到前綴和,這樣能夠把區間求和降到常數複雜度,然而區間加仍是\(O(N)\);這時就須要線段樹登場了(不知道爲啥排版變得巨醜,你們將就一下吧)node
線段樹是一種實用的數據結構,它能夠快速地處理區間操做,維護區間信息。線段樹是一棵二叉樹,它的每個節點存儲的是一個區間的信息(如區間和, 左右端點等),以下圖所示ios
筆者我的比較習慣用結構體來定義每個節點,若是隻開\(2N\)個節點,有一些狀況是不夠的,索性開到\(4N\),並從上到下,從左向右進行編號,根節點編號爲1,其左兒子是2,右兒子是3,以次類推:數據結構
#define ls (k << 1) // 左兒子 #define rs (ls | 1) // 右兒子 struct Node { int l, r, sum, lazy; // l爲左端點,r爲端點,sum是區間和, lazy是懶標記下文會講 Node() {} Node(int _l, int _r, int _sum, int _lazy=0) : l(_l), r(_r), sum(_sum), lazy(_lazy) {} inline int length() {return r - l + 1; } // 返回區間長度 inline ll mi() { return (l + r) >> 1; } // 返回中間點 } node[N << 2];
每次更新了較低一層的區間信息時,須要維護其父節點的信息,好比區間信息爲區間和\(sum\)時,維護時父節點的\(sum\)值等於其左右兒子的\(sum\)值的和post
inline update(int k) { node[k].sum = node[ls].sum + node[rs].sum; }
建樹從最上一層節點開始向下,一旦遇到葉子節點(區間長度爲1的點),說明到最底層了,則返回,再遞歸地更新其父節點的區間信息ui
void build(int l, int r, int k) { // k是編號 if(l == r) { // 葉子節點,輸入它的值並返回 scanf("%d", &a); node[k] = Node(l, r, a); return ; } node[k].l = l; node[k].r = r; int mid = node[k].mi(); build(l, mid, ls); build(mid + 1, r, rs); update(k); }
(注意區分等待加的區間\([l,r]\)和節點\(k\)上的區間\([node[k].l, node[k].r]\)!!)在區間\([l,r]\)上加\(addnum\):從根節點開始,若是咱們所在的節點的區間\([node[k].l, node[k].r] \subseteq [l,r]\),那麼說明這個節點區間的每一個值都須要被加\(addnum\);不然,說明節點上的區間沒有被徹底包含在\([l,r]\)中,若是\(r>mid(mid是節點的區間中值)\),說明區間\([mid + 1, r]\)這個區間還須要加上\(addnum\),因此進入右兒子節點;若是\(l <= mid\),說明區間\([l, mid]\)這個區間還須要加上\(addnum\),因此進入右兒子節點。須要注意的是,後兩種狀況徹底有可能同時知足。咱們再仔細考慮區間加,爲了維護線段樹使其知足左右兒子的\(sum\)之和等於父節點的\(sum\),將父節點的\(sum\)更新以後應該要把它的全部子節點都更新,再用一下上面的圖,好比說咱們讓\([6, 10]\)加10,那爲了維護線段樹,\([6, 10]\)的子節點們都須要加10,總共須要9次加操做,這形成了一個很嚴重的問題:這樣的區間加甚至比暴力還要慢!一個本來是\(O(N)\)的操做被咱們改進成了\(O(NlogN)\),這時,一個重要的思想出現:懶標記。它的思想是先僅維護最上一層的區間信息,而延遲對其子節點的更新,這樣作的好處在於能夠把區間加累積起來,等有須要時將懶標記下傳一次性更新子節點,從而有效下降複雜度spa
inline void push(int k) { // 懶標記下傳 node[ls].lazy = node[rs].lazy = node[k].lazy; node[ls].sum += node[ls].length() * node[k].lazy; node[rs].sum += node[rs].length() * node[k].lazy; node[k].lazy = 0; } inline void add(int k) { if(node[k].l >= l && node[k].r <= r) { // 徹底包含 node[k].sum += node[k].length() * addnum; node[k].lazy += addnum; // 懶標記 return ; } if(node[k].lazy) push(k); // 下傳 if(r > node[k].mi()) add(rs); if(l <= node[k].mi()) add(ls); // 不能是else if update(k); }
區間求和的步驟基本和區間加同樣,代碼也是十分相似code
inline int query(int k) { if(node[k].l >= l && node[k].r <= r) return node[k].sum; int ans = 0; if(node[k].lazy) push(k); if(r > node[k].mi()) ans += query(rs); if(l <= node[k].mi()) ans += query(ls); return ans; }
玩整版開了long long,主要是由於不少題區間一求和就容易爆intorm
#include <cstdio> #include <cstring> #include <iostream> #define mid ((l + r) >> 1) #define ls (k << 1) #define rs (k << 1 | 1) typedef long long ll; const int N = 1e6+5; struct Node { ll l, r, sum, lazy; Node() {} Node(ll _l, ll _r, ll _sum, ll _lazy = 0L) : l(_l), r(_r), sum(_sum), lazy(_lazy) {} inline ll length() { return r - l + 1; } inline ll mi() { return (l + r) >> 1; } }node[N << 2]; ll n, m, l, r, addnum; inline ll read() { // 快讀 ll x = 0; char ch = getchar(); while(ch < '0' || ch > '9') ch = getchar(); while(ch >= '0' && ch <= '9') { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return x; } inline void update(int k) { node[k].sum = node[ls].sum + node[rs].sum; } inline void push(int k) { node[ls].lazy += node[k].lazy; node[rs].lazy += node[k].lazy; node[ls].sum += node[k].lazy * node[ls].length(); node[rs].sum += node[k].lazy * node[rs].length(); node[k].lazy = 0L; } void build(int l, int r, int k) { if(l == r) { ll a = read(); node[k] = Node(l, r, a); return ; } node[k].l = l; node[k].r = r; build(l, mid, ls); build(mid + 1, r, rs); update(k); } inline void add(int k) { if(node[k].l >= l && node[k].r <= r) { node[k].sum += node[k].length() * addnum; node[k].lazy += addnum; return ; } if(node[k].lazy) push(k); if(r > node[k].mi()) add(rs); if(l <= node[k].mi()) add(ls); update(k); } inline ll query(int k) { if(node[k].l >= l && node[k].r <= r) return node[k].sum; ll ans = 0L; if(node[k].lazy) push(k); if(r > node[k].mi()) ans += query(rs); if(l <= node[k].mi()) ans += query(ls); return ans; } int main() { n = read(), m = read(); build(1L, n, 1L); while(m--) { ll type; type = read(); l = read(), r = read(); if(type == 2L) // 區間查詢 printf("%lld\n", query(1L)); else if(type == 1L) { // 區間加 addnum = read(); add(1L); } } return 0; }
區間加 + 區間求和,這是最基本的線段樹,板子題luogu 3372blog
區間修改 + 區間求最值,若是沒有區間修改,那打個ST就好了(不知道ST的話能夠百度一下,不少博客都講得很清楚),常數還小,有修改就用線段樹就行,維護也很簡單,取個max就好了
\[ (a_i + x)^2 = a_i^2+2x*a_i+x^2 \\ \sum_{i=l}^{r}((a_i+x)^2) = \sum_{i=l}^{r}a_i^2+2x*\sum_{i=l}^{r}a_i+(l-r+1)*x^2 \]
能夠按照上面的公式維護\(\sum a_i\)和\(\sum a^2_i\),立方相似,題目HDU 4578 Transformation
開根號操做會讓區間裏的值變得更加接近,那隻要同時維護區間\(max\)和\(min\),若是\(max = min\),\(sum = length * max\);若是\(max \neq min\),就暴力地把這個區間上的數都開方,維護懶標記開方次數,且由於一個數\(n\)最多被開方\(logn\)次就會變成1,因此每一個數暴力其實最多\(O(logn)\),不會超時。區間除的思想也是同樣的,由於除法也會使得區間裏的值變得更加接近
然而線段樹的不少題都結合了各類技巧,以下面這道:
思路:離散化+線段樹