# 線段樹1 by: hush --- ## 概念 ---- 線段樹(segment/interval tree),是一種平衡二元樹,可支援**連續區間型的可分治操作** ---- ### 適用問題 - 區間總和查詢 - 區間極值查詢 - 區間加值/改值 - 區間最大連續和 - 區間開根號 - 其他具有可分治性的區間問題 --- ## 架構 ---- 示意圖 ![segment_tree](https://hackmd.io/_uploads/SJLFiNG1le.png =60%x) ---- - 一棵二元樹:每個節點都對應一個區間 - 根節點對應總區間 - 左子節點對應左半區間,右子節點亦同 - 葉節點區間長度為1 - 每個節點通常存:當前區間的答案(、合併答案的資訊) ---- 實作上通常會用陣列儲存線段樹,$a[1]$為根節點,$a[i]的左子節點為a[i\times 2]$,$右子節點為a[i\times 2+1]$這樣只需要開$a[4\times N]$就夠了($N$為總區間長度) ---- 示意圖 ![segmenttree_2](https://hackmd.io/_uploads/H1oUbHMylg.png =80%x) --- ## 範例1 ---- 接下來會以RMQ(區間最大值)舉例,會有一個初始陣列$a[n]$ ($n\le 10^5$),接下來會有$q$ ($q\le 10^5$)筆操作,可能是把$a[i]$的值修改成$x$或詢問$a[l]$~$a[r]$的最大值 實作上我習慣seg陣列為1-base,區間為左閉右開 ---- ### 建樹函式 ---- ```cpp=1 constexpr int MAXN=100005; int seg[MAXN<<2],a[MAXN]; //a<<2 == a*4 void build(int cur,int l,int r) //[l,r) { ``` - seg存該節點(區間)的最大值,a為初始陣列 - cur為l~r區間在seg[]的索引(每個cur唯一對應一個區間) ---- ```cpp=4 void build(int cur,int l,int r) //[l,r) { if (l+1==r) //leaf node { seg[cur]=a[l]; //唯一的元素就是最大值 return; } int mid=(l+r)>>1; build(cur*2,l,mid); //cur*2為左節點 build(cur*2+1,mid,r); //cur*2+1為右節點 seg[cur]=max(seg[cur*2],seg[cur*2+1]); } ``` ---- 時間複雜度:跑過所有$4n$個節點所以$O(n)$ ---- ### 修改函式 ---- ```cpp=18 void update(int cur,int l,int r,int i,int x) { ``` - i是要修改的位置,x是修改的值 ---- ```cpp=18 void update(int cur,int l,int r,int i,int x) { if (l+1==r) //此時l==i { seg[cur]=x; return; } int mid=(l+r)>>1; if (i<mid) update(cur*2,l,mid,i,x); //i在左區間 else update(cur*2+1,mid,r,i,x); //i在右區間 seg[cur]=max(seg[cur*2],seg[cur*2+1]); } ``` ---- 時間複雜度:一個level只拜訪一個節點 總共$\log(n)$個level所以$O(\log n)$ ---- ### 查詢函式 ---- ```cpp=32 int query(int cur,int l,int r,int ql,int qr) { ``` - $ql, qr$代表詢問的區間,我習慣$[ql,qr)$ ---- ```cpp=32 int query(int cur,int l,int r,int ql,int qr) { if (r<=ql || l>=qr) return -1e18;//當前區間不在詢問區間內 if (ql<=l && r<=qr) return seg[cur]; //當前區間完全在詢問區間內 int mid=(l+r)>>1; return max(query(cur*2,l,mid,ql,qr),query(cur*2+1,mid,r,ql,qr)); } ``` ---- 時間複雜度:$O(\log n)$有點難解釋 ---- ```cpp=41 signed main() { int n,q; cin >> n >> q; for (int i=1;i<=n;i++) cin >> a[i]; build(1,1,n+1); //1-base, [l,r) while (q--) { int op; cin >> op >> ; if (op==1) { int i,x; cin >> i >> x; update(1,1,n+1,i,x); } else { int ql,qr; cin >> ql >> qr; cout << query(1,1,n+1,ql,qr+1) << endl; //[ql,qr) } } } ``` ---- - [範例題目連結](https://cses.fi/problemset/task/1649/) - [CSDC408(我出的類題)](https://csdcojweb1.xi11.cc/problem/408) --- ## 範例2 ---- 給一個$n$個整數的陣列,處理$q$筆以下兩種操作: 1. 將位置$k$的值更新為$u$ 2. 查詢區間$[a,b]$中所有值的總和 ---- ```cpp=1 constexpr int MAXN=100005; int seg[MAXN<<2],a[MAXN]; //seg為區間總和 void build(int cur,int l,int r) //[l,r) { if (l+1==r) //leaf node { seg[cur]=a[l]; //唯一的元素就是加總 return; } int mid=(l+r)>>1; build(cur*2,l,mid); //cur*2為左節點 build(cur*2+1,mid,r); //cur*2+1為右節點 seg[cur]=seg[cur*2]+seg[cur*2+1]; } void update(int cur,int l,int r,int i,int x) { if (l+1==r) //此時l==i { seg[cur]=x; return; } int mid=(l+r)>>1; if (i<mid) update(cur*2,l,mid,i,x); //i在左區間 else update(cur*2+1,mid,r,i,x); //i在右區間 seg[cur]=seg[cur*2]+seg[cur*2+1]; } int query(int cur,int l,int r,int ql,int qr) { if (r<=ql || l>=qr) return 0; if (ql<=l && r<=qr) return seg[cur]; int mid=(l+r)>>1; return query(cur*2,l,mid,ql,qr)+query(cur*2+1,mid,r,ql,qr); } signed main() { int n,q; cin >> n >> q; for (int i=1;i<=n;i++) cin >> a[i]; build(1,1,n+1); //1-base, [l,r) while (q--) { int op; cin >> op >> ; if (op==1) { int i,x; cin >> i >> x; update(1,1,n+1,i,x); } else { int ql,qr; cin >> ql >> qr; cout << query(1,1,n+1,ql,qr+1) << endl; //[ql,qr) } } } ``` ---- - [範例題目連結](https://cses.fi/problemset/task/1648) - [zj_d799(需要腦洞大開)](https://zerojudge.tw/ShowProblem?problemid=d799) --- ## 變化題 ---- 1.區間最早值 - 給一個$n$個整數的陣列,處理$q$筆詢問: 對於詢問值$x$,找出第一個$\ge x$個值的位置,並把該位置減去$x$後繼續下一次詢問 - 技巧:在線段樹上二分搜 ---- 方法: - 一個維護區間最大值的線段樹(RMQ) - 詢問時若左半區間最大值$\ge x$則答案在左邊,否則在右邊 - 遞迴直到葉節點 ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define lc cur<<1 #define rc cur<<1|1 #define mid (l+r)>>1 using namespace std; int seg[(200000<<2)+5],ans=0; void update(int cur,int l,int r,int i,int x) //add { if (l+1==r) { seg[cur]+=x; return; } if (i<mid) update(lc,l,mid,i,x); else update(rc,mid,r,i,x); seg[cur]=max(seg[lc],seg[rc]); } void query(int cur,int l,int r,int x) { if (seg[cur]<x) return; if (l+1==r) { ans=l; return; } query(lc,l,mid,x); if (!ans) query(rc,mid,r,x); } signed main() { //colinorz; int n,m; cin >> n >> m; for (int i=1;i<=n;i++) { int h; cin >> h; update(1,1,n+1,i,h); } for (;m--; ans=0) { int r; cin >> r; query(1,1,n+1,r); cout << ans << " \n"[m==0]; if (ans) update(1,1,n+1,ans,-r); } } ``` ---- [範例題目連結(CSES)](https://cses.fi/problemset/task/1143/) [範例題目連結(CF)](https://codeforces.com/edu/course/2/lesson/4/2/practice/contest/273278/problem/C) ---- 2.區間最大差 - 單點修改,詢問區間內任兩值的差最大可以是多少 - 方法:同時維護區間最大及最小值,詢問時相減及為答案 ---- 3.區間最大順向差 - 單點修改,詢問區間內任兩值後面減前面最大可以是多少 - 方法:同時維護區間最小值、最大值及答案(最大順向差),詢問時答案是$\max(左區間答案,右區間答案,右最大-左最小)$ ---- 4.區間最大連續和 - 沒有修改,多筆詢問 - 方法1:區間和$=$前綴和順向差,所以答案就是前綴和陣列的區間最大順向差 - 方法2:維護最大前綴和、最大後綴和、區間和及答案(最大連續和),詢問時答案是 $\max(左區間答案,右區間答案,左後綴+右前綴)$ --- ## 練習題 ---- - [CSES2206](https://cses.fi/problemset/task/2206) - [CSES1561](https://cses.fi/problemset/task/1651/) - [zj_f986(d799的進階)](https://zerojudge.tw/ShowProblem?problemid=f986) ---- - CSES2206題解: 此題為RMQ類題,維護$p_a+a$和$p_a-a$的區間最小值,答案就是$p_a+a-b,a\ge b$最小值和$p_a-a+b,a\le b$最小值取最小值 ---- CSES2206 AC code ```cpp= #include<bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define lc cur<<1 #define rc cur<<1|1 #define mid (l+r)>>1 using namespace std; int seg[5][(200000<<2)+5]; //seg[0]->pa+a,seg[1]->pa-a void update(int tr,int cur,int l,int r,int i,int x) { if (l+1==r) { seg[tr][cur]=(tr?-i:i)+x; return; } if (i<mid) update(tr,lc,l,mid,i,x); else update(tr,rc,mid,r,i,x); seg[tr][cur]=min(seg[tr][lc],seg[tr][rc]); } int query(int tr,int cur,int l,int r,int ql,int qr) { if (qr<=l || r<=ql) return 1e9; if (ql<=l && r<=qr) return seg[tr][cur]; return min(query(tr,lc,l,mid,ql,qr),query(tr,rc,mid,r,ql,qr)); } signed main() { //colinorz; int n,q; cin >> n >> q; for (int i=1;i<=n;i++) { int p; cin >> p; update(0,1,1,n+1,i,p); update(1,1,1,n+1,i,p); } while (q--) { int op,k,x; if (cin>>op>>k && op==1) { cin >> x; update(0,1,1,n+1,k,x); update(1,1,1,n+1,k,x); } else cout << min(query(0,1,1,n+1,k,n+1)-k, query(1,1,1,n+1,1,k+1)+k) << endl; } } ``` ---- - CSES1561題解: 差分序列的前綴和$=$原序列,維護差分序列$d$的前綴和,$[l,r]$都加$x$就是$d[l]+x,d[r+1]-x$ 前綴和可以用區間和維護或是用[BIT](https://cp.wiwiho.me/fenwick-tree/)(常數佳) ---- CSES1561 AC code ```cpp= #include<bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define lc cur<<1 #define rc cur<<1|1 #define mid (l+r)>>1 using namespace std; int seg[(200000<<2)+5]={}; //seg[i]->d[i] void update(int cur,int l,int r,int i,int x) { if (l+1==r) { seg[cur]+=x; return; } if (i<mid) update(lc,l,mid,i,x); else update(rc,mid,r,i,x); seg[cur]=seg[lc]+seg[rc]; } int query(int cur,int l,int r,int ql,int qr) { if (r<=ql || l>=qr) return 0; if (ql<=l && r<=qr) return seg[cur]; return query(lc,l,mid,ql,qr)+query(rc,mid,r,ql,qr); } signed main() { //colinorz; int n,q,lst=0,a; cin >> n >> q; for (int i=1;i<=n;i++,lst=a) { cin >> a; update(1,1,n+2,i,a-lst); //n+2避免qr+1溢位 } while (q--) { int op; cin >> op; if (op==1) { int ql,qr,x; cin >> ql >> qr >> x; update(1,1,n+2,ql,x); update(1,1,n+2,qr+1,-x); } else { int i; cin >> i; cout << query(1,1,n+2,1,i+1) << endl; } } } ``` ---- ---- - zj_f986題解: $L,R$為修改的部分,$D(x)$代表位置$x$前綴和的變化量,整理之後會變二次多項式,分別維護三個項的係數即可 $2D(x) =\begin{cases} 0, & \text{if } x < L \\ (x - L + 1)(x - L + 2), & \text{if } L \le x \le R \\ (R - L + 1)(R - L + 2), & \text{if } x > R \end{cases}$ ---- f986 AC code ```cpp= #include<bits/stdc++.h> #define int long long #define lc cur<<1 #define rc cur<<1|1 #define mid (l+r)>>1 using namespace std; int a[200005]={}; int seg[5][(200000<<2)+5]={}; //seg[0]->a, seg[1]->b, seg[2]->c void build(int cur,int l,int r) { if (l+1==r) { seg[2][cur]=a[l]<<1; return; } build(lc,l,mid); build(rc,mid,r); seg[2][cur]=seg[2][lc]+seg[2][rc]; } void update(int tr,int cur,int l,int r,int i,int x) { if (l+1==r) { seg[tr][cur]+=x; return; } if (i<mid) update(tr,lc,l,mid,i,x); else update(tr,rc,mid,r,i,x); seg[tr][cur]=seg[tr][lc]+seg[tr][rc]; } int query(int tr,int cur,int l,int r,int ql,int qr) { if (qr<=l || ql>=r) return 0; if (ql<=l && r<=qr) return seg[tr][cur]; return query(tr,lc,l,mid,ql,qr)+query(tr,rc,mid,r,ql,qr); } inline int pre(int n,int i) { return i*i*query(0,1,1,n+1,1,i+1)+i*query(1,1,1,n+1,1,i+1)+query(2,1,1,n+1,1,i+1); } signed main() { int n,q; cin >> n >> q; for (int i=1;i<=n;i++) cin >> a[i]; build(1,1,n+1); while (q--) { int c,ql,qr; if (cin>>c>>ql>>qr && c==1) { /* if (x<R) 2d(x)=(x-L+1)*(x-L+2)=x^2+ (-2L+3)x+ (L-2)*(L-1) if (x>=R) 2d(x)=(R-L+1)*(R-L+2) */ update(0,1,1,n+1,ql,1); update(0,1,1,n+1,qr,-1); update(1,1,1,n+1,ql,3-2*ql); update(1,1,1,n+1,qr,2*ql-3); update(2,1,1,n+1,ql,(ql-2)*(ql-1)); update(2,1,1,n+1,qr,(ql-2)*(1-ql)); update(2,1,1,n+1,qr,(qr-ql+1)*(qr-ql+2)); //x>=qr } else cout << ((pre(n,qr)-pre(n,ql-1))>>1) << endl; } } ``` --- # 謝謝大家
{"description":"by: hush","contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":12986,\"del\":2358}]","title":"線段樹1"}
    91 views