# 線段樹2 by:hush --- ## 懶惰標記(lazy tag) ---- - 先看一個問題: 給一個序列(長度$n\le 2\cdot 10^5$),做$q\le 10^5$筆操作,對指定區間內的每個元素加某個數$x$或是詢問某區間的和 一個一個加肯定TLE! ---- 並不是每個區間都需要算!哪些才需要算? - Ans: 會用到的再算,因為詢問次數不會TLE,你只算問到的也不會 ---- ### 怎麼達成「只算需要的」? - 用一種叫做「懶惰標記」(lazy tag)的技巧 - 想法:不是每次都真的去加值,而是記下「這個區間以後要加多少」 - 查詢或更新到更下面區間時,再把之前欠的值下傳下去(push) ---- ### 懶惰標記的結構 - 每個節點都多記一個`tag`值,表示「這個區間每個值要多加多少」 - 當下推的時候,再把這些加到子節點上,更新子節點的 sum 值 - <span style="color:red">注意:該區間的tag只會影響子孫區間</span> ---- Code(前置部分) ```cpp= struct node { int sum=0,tag=0; } seg[500005<<2]; void addtag(int cur,int x,int len) //把區間加上懶標 { seg[cur].sum+=x*len; //修改當前區間 seg[cur].tag+=x; } void push(int cur,int l,int mid,int r) { //把懶標打給兩個子節點 addtag(lc,seg[cur].tag,mid-l); addtag(rc,seg[cur].tag,r-mid); seg[cur].tag=0; } ``` ---- Code(修改函式) ```cpp= void update(int cur,int l,int r,int ql,int qr,int x) { if (r<=ql || l>=qr) return; if (ql<=l && r<=qr) { addtag(cur,x,r-l); return; } if (seg[cur].tag!=0) push(cur,l,mid,r); //更新子區間的值 update(lc,l,mid,ql,qr,x); update(rc,mid,r,ql,qr,x); seg[cur].sum=seg[lc].sum+seg[rc].sum; } ``` ---- Code(詢問函式) ```cpp= 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].sum; if (seg[cur].tag) push(cur,l,mid,r); //更新子區間的值 return query(lc,l,mid,ql,qr)+query(rc,mid,r,ql,qr); } ``` ---- ### 時間複雜度 每次修改或詢問都是$O(\log n)$的時間 ---- ### 例題1 - 題目([zj_d799](https://zerojudge.tw/ShowProblem?problemid=d799)):區間加值,區間求和 如果有時間可以看看底下不同做法 ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #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; struct node { int sum=0,tag=0; } seg[500005<<2]; void addtag(int cur,int x,int len) //把區間加上懶標 { seg[cur].sum+=x*len; //修改當前區間 seg[cur].tag+=x; } void push(int cur,int l,int mid,int r) { //把懶標打給兩個子節點 addtag(lc,seg[cur].tag,mid-l); addtag(rc,seg[cur].tag,r-mid); seg[cur].tag=0; } void update(int cur,int l,int r,int ql,int qr,int x) { if (r<=ql || l>=qr) return; if (ql<=l && r<=qr) { addtag(cur,x,r-l); return; } if (seg[cur].tag!=0) push(cur,l,mid,r); //更新子區間的值 update(lc,l,mid,ql,qr,x); update(rc,mid,r,ql,qr,x); seg[cur].sum=seg[lc].sum+seg[rc].sum; } 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].sum; if (seg[cur].tag) push(cur,l,mid,r); //更新子區間的值 return query(lc,l,mid,ql,qr)+query(rc,mid,r,ql,qr); } signed main() { colinorz; int n,q,v,ql,qr,x; cin >> n; for (int i=0;i<n;i++) { cin >> x; update(1,0,n,i,i+1,x); } cin >> q; while (q--) { if (cin>>v>>ql>>qr && v==1) { cin >> x; update(1,0,n,ql-1,qr,x); } else cout << query(1,0,n,ql-1,qr) << endl; } } ``` --- ## 修改順序 ---- - 題目變難了: 長度為$n\le 2\cdot 10^5$的序列,處理以下操作: 1.將區間$[l,r]$中的每個值 **增加$x$**。 2.將區間$[l,r]$中的每個值 **變成$x$**。 3.計算區間$[l,r]$中所有值的 **總和**。 ---- - 想法:我們需要兩個懶標,一個代表增加多少,一個代表修改成多少 - 問題:在`push`時要先 **修改** 區間值還是先 **增加** 區間值? 答:<span class="fragment">一般來說「先改再加」較好寫</span> ---- ### 實作方法 - 加值時:直接在標記加值的tag上加值 - 改值時:先清空加值的tag,再修改改值的tag - push時(順序不能變): 1.若能改值則push改值的tag 2.若能加值則push加值的tag ---- Code(push函式) ```cpp= struct node { int sum=0,tag1=0,tag2=0; //tag1->alt,tag2->add } seg[(200000<<2)+5]; inline void addtag1(int cur,int l,int r,int x) { seg[cur].sum=x*(r-l),seg[cur].tag1=x; seg[cur].tag2=0; //將加值設為0!!! } inline void addtag2(int cur,int l,int r,int x) { seg[cur].sum+=x*(r-l),seg[cur].tag2+=x; } void push(int cur,int l,int r) { if (seg[cur].tag1!=0) //tag1代表改值 { addtag1(lc,l,mid,seg[cur].tag1); addtag1(rc,mid,r,seg[cur].tag1); seg[cur].tag1=0; } if (seg[cur].tag2!=0) //tag2代表加值 { addtag1(lc,l,mid,seg[cur].tag2); addtag1(rc,mid,r,seg[cur].tag2); seg[cur].tag2=0; } } ``` ---- ### 例題2 - 題目([CSES1735](https://cses.fi/problemset/task/1735/)):區間加值、改值,區間求和 ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #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; struct node { int sum=0,tag1=0,tag2=0; //tag1->alt,tag2->add } seg[(200000<<2)+5]; inline void addtag1(int cur,int l,int r,int x) { seg[cur].sum=x*(r-l),seg[cur].tag1=x,seg[cur].tag2=0; } inline void addtag2(int cur,int l,int r,int x) { seg[cur].sum+=x*(r-l),seg[cur].tag2+=x; } void push(int cur,int l,int r) { int &x1=seg[cur].tag1,&x2=seg[cur].tag2; if (x1) addtag1(lc,l,mid,x1),addtag1(rc,mid,r,x1),x1=0; if (x2) addtag2(lc,l,mid,x2),addtag2(rc,mid,r,x2),x2=0; } void alt(int cur,int l,int r,int ql,int qr,int x) { if (r<=ql || l>=qr) return; if (ql<=l && r<=qr) { addtag1(cur,l,r,x); return; } push(cur,l,r); alt(lc,l,mid,ql,qr,x); alt(rc,mid,r,ql,qr,x); seg[cur].sum=seg[lc].sum+seg[rc].sum; } void add(int cur,int l,int r,int ql,int qr,int x) { if (r<=ql || l>=qr) return; if (ql<=l && r<=qr) { addtag2(cur,l,r,x); return; } push(cur,l,r); add(lc,l,mid,ql,qr,x); add(rc,mid,r,ql,qr,x); seg[cur].sum=seg[lc].sum+seg[rc].sum; } 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].sum; push(cur,l,r); return query(lc,l,mid,ql,qr)+query(rc,mid,r,ql,qr); } signed main() { colinorz; int n,q; cin >> n >> q; for (int i=1;i<=n;i++) { int a; cin >> a; add(1,1,n+1,i,i+1,a); } while (q--) { int op,a,b,x; if (cin>>op>>a>>b && 3==op) cout << query(1,1,n+1,a,b+1) << endl; else if (cin>>x && 2==op) alt(1,1,n+1,a,b+1,x); else add(1,1,n+1,a,b+1,x); } } ``` --- ## 動態開點 ---- - 當序列長度非常大(例如$n\le 10^9$),無法預先建立完整的線段樹。 - 解法:**動態開點**,僅在需要用到時才建立節點,節省空間。 ---- ### 動態開點的結構 開一個`struct`存當前節點(區間)的資訊,並利用指標指向兩個子節點 ---- - 例題3([CSES1144](https://cses.fi/problemset/task/1144)): 給你一個序列(長度$n\le 2\cdot 10^5$),序列值$\le 10^9$,處理兩種操作: 1.單點修改某個值。 2.查詢序列中有幾個值在區間$[l,r]$內。 ---- - 解法: 將$[1,10^9]$視為一個線段,某個點為0代表沒人,1則是有人 題目可轉換為求$[l,r]$有幾個1(區間和) 此題也可用離線||(其實這是官解)|| - 這方法常數大,~~可用pragma壓常數~~ ---- Code(前置部分) ```cpp= struct node { int val=0; node *lc=0,*rc=0; void pull() { val=0; for (auto i : {lc,rc}) if (i) val+=i->val; } } *rt=0; ``` ---- Code(修改及詢問) ```cpp= void update(node*& cur,int l,int r,int i,int x) { if (!cur) cur=new node; if (l+1==r) { cur->val+=x; return; } if (i<mid) update(cur->lc,l,mid,i,x); else update(cur->rc,mid,r,i,x); cur->pull(); } int query(node*& cur,int l,int r,int ql,int qr) { if (qr<=l || ql>=r || !cur) return 0; if (ql<=l && r<=qr) return cur->val; return query(cur->lc,l,mid,ql,qr)+query(cur->rc,mid,r,ql,qr); } ``` ---- AC code ```cpp= #include<bits/stdc++.h> #pragma GCC optimize("O3") #pragma GCC target("sse,sse2,ssse3,sse4,avx,avx2") #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define mid (l+r)>>1 using namespace std; constexpr int MAXN=1e9+5; struct node { int val=0; node *lc=0,*rc=0; void pull() { val=0; for (auto i : {lc,rc}) if (i) val+=i->val; } } *rt=0; void update(node*& cur,int l,int r,int i,int x) { if (!cur) cur=new node; if (l+1==r) { cur->val+=x; return; } if (i<mid) update(cur->lc,l,mid,i,x); else update(cur->rc,mid,r,i,x); cur->pull(); } int query(node*& cur,int l,int r,int ql,int qr) { if (qr<=l || ql>=r || !cur) return 0; if (ql<=l && r<=qr) return cur->val; return query(cur->lc,l,mid,ql,qr)+query(cur->rc,mid,r,ql,qr); } int a[200005]; signed main() { colinorz; int n,q; cin >> n >> q; for (int i=1;i<=n;i++) { cin >> a[i]; update(rt,1,MAXN,a[i],1); } while (q--) { char op; int k,x; if (cin>>op>>k>>x && op=='!') { update(rt,1,MAXN,a[k],-1); a[k]=x; update(rt,1,MAXN,a[k],1); } else cout << query(rt,1,MAXN,k,x+1) << endl; } } ``` --- # 謝謝大家
{"contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":9180,\"del\":531}]","title":"線段樹2","description":"by:hush"}
    86 views