線段樹
進階資結
\(225\)賴泓安
課程大綱
- 題目
- 線段樹優化建圖
- 時間線段樹
- Pattern
- Segment tree beats
IOIC2023進階資結課程
去年資讀簡報
reference
暖身題目
給一個長為\(N\)的序列\(A\),有\(Q\)筆詢問
每次輸出\([l,r]\)之間的絕對眾數,不存在則輸出\(0\)
\(N,Q,a_i \leq 5\cdot10^5\)
絕對眾數:出現數目嚴格大於所有數字的一半
性質
假設只有一筆詢問
我們做個神奇操作
紀錄一開始的數字\(x = a_l\),並給它一個生命值\(hp = 1\)
然後往右掃
如果遇到的數字\(a_i = x\),\(hp\)++,否則\(hp\)--
若\(hp\)已經\(=0\),則設\(x = a_i\),\(hp\)重設為\(1\)
性質
只有做完後留下來的\(x\)有可能是答案!
可以線性 或 預處理+二分搜驗
為什麼?
注意到,我們把兩個相異元素\(x,y\)一起去除時
並不會影響絕對眾數的地位
如果\(x,y\)都不是絕對眾數,那沒有差。
如果其中一個是絕對眾數(假設有\(k\)個)
原本:\(k > \frac{N}{2}\)
刪除後的比例:\( (k-1):(N-2) \)
\((k-1) > \frac{N-2}{2} \)仍然成立
多筆詢問
用線段樹維護區間內的\(x和hp\)
對每個詢問\([l,r]\),各個樹上的子區間是可以合併的
所以能夠得到最終留下來的\(x\)
再二分搜確認是否為答案
code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
#define pii pair<int,int>
int arr[500005];
vector <pair<int,int> > sg;
pii init(int l, int r, int node) {
if (l == r) {
sg[node].first = arr[l];
sg[node].second = 1;
return sg[node];
}
int mid = (l+r)>>1;
pii lp = init(l,mid,node<<1);
pii rp = init(mid+1,r,(node<<1)+1);
if (lp.first == rp.first) {
sg[node].first = lp.first;
sg[node].second = lp.second+rp.second;
} else {
if (lp.second >= rp.second) {
sg[node].first = lp.first;
sg[node].second = lp.second-rp.second;
} else {
sg[node].first = rp.first;
sg[node].second = rp.second-lp.second;
}
}
return sg[node];
}
pii query(int ql, int qr, int l, int r, int node) {
if (ql <= l && r <= qr) {
return sg[node];
}
int mid = (l+r)>>1;
if (qr <= mid) {
return query(ql,qr,l,mid,node<<1);
}
if (ql > mid) {
return query(ql,qr,mid+1,r,(node<<1)+1);
}
pii lp = query(ql,qr,l,mid,node<<1);
pii rp = query(ql,qr,mid+1,r,(node<<1)+1);
pii ret;
if (lp.first == rp.first) {
ret.first = lp.first;
ret.second = lp.second+rp.second;
} else {
if (lp.second >= rp.second) {
ret.first = lp.first;
ret.second = lp.second-rp.second;
} else {
ret.first = rp.first;
ret.second = rp.second-lp.second;
}
}
return ret;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, q;
cin>>n>>q;
vector <vector<int> > pos(500005);
sg.resize(n<<2);
for (int i = 1; i <= n; ++i) {
cin>>arr[i];
pos[arr[i]].push_back(i);
}
init(1,n,1);
while(q--) {
int l, r;
cin>>l>>r;
pii pa = query(l,r,1,n,1);
auto al = lower_bound(pos[pa.first].begin(),pos[pa.first].end(),l);
auto ar = upper_bound(pos[pa.first].begin(),pos[pa.first].end(),r);
int count = ar-al;
if (count > ((r-l+1)>>1)) {
cout<<pa.first<<endl;
} else {
cout<<0<<endl;
}
}
return 0;
}
如果你要難一點的
線段樹優化建圖
先上題目
題敘
給定一張原本無邊的圖,有三種操作
\(1.\)連接一條\(u,v\)之間邊權\(w\)的邊
\(2.\)連接所有\([l,r]\)到\(v\)之間邊權\(w\)的邊
\(3.\)連接\(u\)到所有\([l,r]\)之間邊權\(w\)的邊
最後求單點源最短路
\(N,Q \leq 10^5, w \leq 10^9\)
問題是圖怎麼建?
最短路顯然是要Dijkstra
線段樹的本質?
每個線段樹上的節點
都代表某段連續區間的所有點
反過來說,針對給定的一段連續區間
我們可以將它分解成線段樹上的最多\(logN\)個節點
然後我們就會做了
先建兩棵線段樹,分別作為入度和出度使用
入度那棵往子節點連無權邊
出度那棵往父節點連無權邊
入度線段樹
1
2
3
4
5
6
7
8
出度線段樹
1
2
3
4
5
6
7
8
加邊操作
\(1.\)區間連到點\(v\):
從負責出度的那棵線段樹上,找到一些節點代表當前區間
從這些節點連邊到代表\(v\)的葉節點
\(2.\)點\(u\)連到區間:
從負責入度的那棵線段樹上,找到一些節點代表當前區間
由代表\(u\)的葉節點連邊回去
點連到區間
1
2
3
4
5
6
7
8
2 -> [1,6]
8 -> [5,7]
區間連到點
1
2
3
4
5
6
7
8
[1,2] -> 3
[1,5] -> 7
複雜度
每次操作最多連\(log N\)條邊
操作複雜度\(O(Q log N)\)
總節點數會是\(O(N)\)的
最後跑一遍Dijsktra
code
#pragma GCC optimize("O3")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define p_q priority_queue
#define endl '\n'
#define pb push_back
#define mid ((l+r)>>1)
#define int ll
int mpk;
vector<vector< pii > > g;
struct SEG{
int sz, t;
void setedge(int l, int r, int p) {
if (l == r) {
if (t) g[p+mpk].pb({(p>>1)+mpk,0});
else {
g[p].pb({p+mpk,0});
g[p+mpk].pb({p,0});
}
return;
}
setedge(l,mid,p<<1);
setedge(mid+1,r,p<<1|1);
if (!t) {
g[p].pb({p<<1,0});
g[p].pb({p<<1|1,0});
} else if (p != 1) {
g[p+mpk].pb({(p>>1)+mpk,0});
}
}
void init(int _n,int _t) {
sz = _n;
t = _t;
setedge(1,_n,1);
}
int find_node(int tar) {
int l = 1, r = sz, p = 1;
while(l != r) {
p <<= 1;
if (tar <= mid) r = mid;
else {
l = mid+1;
p++;
}
}
return t?(p+mpk):p;
}
void add(int l, int r, int p, int v, int ql, int qr, int w) {
if (ql <= l && r <= qr) {
if (!t) g[find_node(v)].pb({p,w});
else g[p+mpk].pb({find_node(v),w});
return;
}
if (qr <= mid) add(l,mid,p<<1,v,ql,qr,w);
else if (ql > mid) add(mid+1,r,p<<1|1,v,ql,qr,w);
else {
add(l,mid,p<<1,v,ql,qr,w);
add(mid+1,r,p<<1|1,v,ql,qr,w);
}
}
} seg[2];
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, q, s;
cin>>n>>q>>s;
mpk = 4*n;
g.resize(2*mpk+5);
seg[0].init(n,0), seg[1].init(n,1);
while(q--) {
int c, v, l, r, w;
cin>>c>>v;
if (c == 1) {
cin>>l>>w;
seg[0].add(1,n,1,v,l,l,w);
} else if (c == 2) {
cin>>l>>r>>w;
seg[0].add(1,n,1,v,l,r,w);
} else {
cin>>l>>r>>w;
seg[1].add(1,n,1,v,l,r,w);
}
}
vector<int> dis(2*mpk+5,2e18), vis(2*mpk+5,0);
dis[seg[0].find_node(s)] = 0;
priority_queue< pii,vector< pii >,greater< pii > > pq;
pq.push({0,seg[0].find_node(s)});
while(!pq.empty()) {
pii cur = pq.top();
pq.pop();
if (vis[cur.second]) continue;
vis[cur.second] = 1;
for (auto [u,w] : g[cur.second]) {
if (dis[u] >= dis[cur.second]+w) {
dis[u] = dis[cur.second]+w;
pq.push({dis[u],u});
}
}
}
for (int i = 1; i <= n; ++i) {
if (i != 1) cout<<' ';
int ans = min(dis[seg[0].find_node(i)],dis[seg[1].find_node(i)]);
if (ans >= 2e18) cout<<-1;
else cout<<ans;
}
cout<<endl;
return 0;
}
時間線段樹
顧名思義
跟值域線段樹差不多的概念
但這次我們以時間為Index開線段樹
一樣先看例題
題敘
給一張圖(\(N\)點\(M\)邊)
有一些加邊或刪邊操作
每次操作完問連通塊個數
\(N \leq 5\)x\(10^5\), \(M+Q \leq 5\)x\(10^5\)
問題就在
如何維護DSU的刪除操作?
DSU在幹嘛
說白一點
DSU的每次加邊操作
其實就是修改一個陣列上的某些值
進一步想,一使用啟發式合併時
每次加邊,陣列上最多只會有一個值被改變
Undo操作
在使用啟發式合併的狀況下
如果對DSU的操作只有回到上一步?
利用stack的概念
我們用一個stack紀錄每次加邊的時候誰被改到
每次的Undo操作
我們只需要把剛剛加邊時改到的值改回來
維護這個DSU的新增和刪除操作就很容易了吧
套用線段樹的結構
每條邊都會有一段存在的時間
我們可以把它拆成\(O(logN)\)個節點標在樹上
然後對線段樹DFS
看個圖
1
2
3
4
5
6
7
8
Time:
+
+
+
+
-
-
-
+
+
code
#pragma GCC optimize("O3")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define p_q priority_queue
#define endl '\n'
#define pb push_back
#define mid ((l+r)>>1)
struct DSU{
vector<int> bs, sz;
vector< pii > hsp;
vector<int> num;
int cnt;
void init(int _n) {
bs.assign(_n+5,0);
sz.assign(_n+5,1);
num.clear(), hsp.clear();
cnt = _n;
for (int i = 1; i <= _n; ++i) bs[i] = i;
}
int find(int p) {
return (p == bs[p])?p:find(bs[p]);
}
void add(int u, int v) {
int a = find(u), b = find(v);
if (a == b) {
hsp.pb({0,0});
num.pb(0);
return;
}
if (sz[a] < sz[b]) swap(a,b);
bs[b] = a;
num.pb(b);
hsp.pb({a,sz[a]});
sz[a] += sz[b];
cnt--;
}
void undo() {
if (num.back()) {
sz[hsp.back().first] = hsp.back().second;
bs[num.back()] = num.back();
cnt++;
}
num.pop_back();
hsp.pop_back();
}
} dsu;
struct SEG{
vector<vector< pii > > a;
void init(int _n) {
a.assign(_n<<2,vector< pii >());
}
void mdf(int l, int r, int p, int ql, int qr, int u, int v) {
if (ql <= l && r <= qr) {
a[p].pb({u,v});
return;
}
if (ql > mid) mdf(mid+1,r,p<<1|1,ql,qr,u,v);
else if (qr <= mid) mdf(l,mid,p<<1,ql,qr,u,v);
else {
mdf(mid+1,r,p<<1|1,ql,qr,u,v);
mdf(l,mid,p<<1,ql,qr,u,v);
}
}
void dfs(int l, int r, int p) {
for (auto [u,v] : a[p]) dsu.add(u,v);
if (l == r) {
if (l) cout<<dsu.cnt<<endl;
for (int i = 0; i < a[p].size(); ++i) dsu.undo();
return;
}
dfs(l,mid,p<<1);
dfs(mid+1,r,p<<1|1);
for (int i = 0; i < a[p].size(); ++i) dsu.undo();
}
} seg;
void slv() {
int n, m, q;
cin>>n>>m>>q;
dsu.init(n);
int a, b;
map< pii,pii > mp;
while(m--) {
cin>>a>>b;
a++, b++;
if (a > b) swap(a,b);
mp[{a,b}].second++;
}
char c;
seg.init(q);
for (int i = 1; i <= q; ++i) {
cin>>c>>a>>b;
a++, b++;
if (a > b) swap(a,b);
if (c == 'N') {
if (!mp.count({a,b}) || !mp[{a,b}].second) mp[{a,b}].first = i;
mp[{a,b}].second++;
} else {
if (--mp[{a,b}].second == 0) {
seg.mdf(0,q,1,mp[{a,b}].first,i-1,a,b);
}
}
}
for (auto it : mp) {
if (!it.second.second) continue;
seg.mdf(0,q,1,it.second.first,q,it.first.first,it.first.second);
}
seg.dfs(0,q,1);
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t; cin>>t;
while(t--) {
slv();
}
return 0;
}
只要是可以支援Undo的
都能套上去喔
例如李超線段樹
Pattern
講師可能也不太會
Pattern?
就是要拆開出題者的包裝
看出題目真正要你處理的
題目
有\(N\)個學生(編號\(1\)~\(N\))由左而右排成一列
現在你要給每個學生一個介於\(1\)~\(N\)的成績
須符合下列條件:
1.由左而右,成績非嚴格遞增
2.學生編號遞增時,成績非嚴格遞增
有\(Q\)筆操作,每筆操作會交換兩位學生的位置
每次操作後輸出分派的成績最多可以有幾種數字
\(N \leq 10^6, Q \leq 3 \cdot10^5\)
例子
學生排列:\([2,3,1,5,4,7,6]\)
一組合法排列:\([1,1,1,2,2,3,3]\)
*有三個不同成績
好像有什麼類似環的結構耶
環的個數代表最多能放的數字種類
觀察相鄰兩人\(u,v\)(左與右)
若\(u > v\),則\([u,v]\)的分數必須一樣
這可以想像成
\([u,v)\)這些人都被\(v\)給控制住了
所以我們的任務變成
要找有多少自由的人
區間加值&詢問最小值個數!
對每組相鄰且\(u > v\)的\(u,v\)
我們對\([u,v)\)做區間加值
最後看有幾個\(0\)就是答案
(\(0\)一定是最小值,所以這等價詢問最小值個數)
可以用類似矩形覆蓋面積的線段樹維護
修改
就當成先刪除再新增就好了
總複雜度:\(O(NlogN+QlogN)\)
題敘
有\(HW\)個座位,安排成\(H\)個橫列和\(W\)個直行。
橫列編號為\(0\)到\(H-1\),直行編號為\(0\)到\(W-1\)。
你邀請了\(HW\)位選手,編號為\(0\)到\(HW-1\),
你有一張座位圖,把選手\(i\)原先都有獨特的座位
座位當中,一個面積為\(k\)的長方形區域為美麗的
表示這個長方形中的選手編號是\(0\)到\(k-1\)
一張座位圖的美麗程度即為座位圖中所能找到的美麗長方形個數
題敘
接著會有\(Q\)筆操作
每次將兩位選手交換位置
請在每次更新過後
輸出當下座位表的美麗程度
subtask:H = 1
我們可以先觀察看看座位為一直線的狀態
先假設所有座位一開始都沒人(記為白色)
然後讓選手一編號入座(該位置改為黑色)
所以我們的任務變成
找有多少時間點,黑色的位置是一段連續區間
符合要求:
不符合要求:
觀察:當一黑一白的1x2方格數恰為2時,即符合條件
我們可以先處理每個1x2方格會維持一黑一白的時間區間
假設該方格上的編號為\(L,R (L < R)\)
則其為黑白狀態的時間為\([L,R)\)
我們可以想像這是一個區間加值
詢問的時候就是看有多少個時間點\(=2\)
另外,只要我們塗上第一個黑色之後
一黑一白的方格一定會\(\geq2\)
所以我們詢問的會是最小值的個數
修改的的話當作刪除再新增處理即可
二維情況
滿足黑色區域為一整塊長方形的條件為
- 2x2且為三白一黑的方形恰有4個
- 不存在三黑一白的格子
符合
不符合
複雜度
建樹:\(O(HW)\)
修改與詢問:\(O(QlogHW)\)
總共:\(O(HW+QlogHW)\)
SEGment tree beats
吉如一線段樹
區間開根號問題
有一個序列\(A\),長度為\(N\)
支援\(Q\)次下列兩種操作
1.詢問\([l,r]\)的區間和
2.把\([l,r]\)所有元素開根號
\(N,Q \leq 3\cdot10^5, a_i \leq 10^{12}\)
難點
我們沒有辦法如往常那樣
隨便打個懶標說
我要把這個區間開根號
觀察
題目沒有把值變大的操作
開根號對某個數字\(C\)來說
最多只要\(O(loglogC)\)次
它就會變成\(1\)了
那就暴力吧
如果一個要被開根號的區間都已經是\(1\)
那我們不用理他
否則就暴力再遞迴下去
這樣最多也只會多暴力\(O(loglogC)\)次
總複雜度:\(O(QlogN+NlogNloglogC)\)
區間取min問題
維護長度為\(N\)的序列\(A\),
支援\(Q\)次下列3種操作
\(N,Q \leq 10^6, a_i \leq 10^{18}\)
- 詢問\([l,r]\)的區間和
- 詢問\([l,r]\)中最大值
- 給定\(x\),對\([l,r]\)裡的\(a_i\),\(a_i = min(a_i,x)\)
\(a_i = min(a_i,x)\)做了什麼?
把區間內大於\(x\)的數字都變成\(x\)
其他的都不改變
如果區間內比\(x\)大的那些數字只有一種
那隨便就很好做
節點資訊
1.最大值 \(mx\)
2.最大值有幾個 \(cnt\)
3.嚴格次大值 \(snd\)
4.區間和 \(sum\)
5.懶人標記 \(tg\)
修改時 \(x \geq mx\)
很顯然我們什麼都不用做
修改時 \(snd < x < mx\)
修改\(mx = x\)
另外再打個懶標就好了
修改時 \(x \leq snd\)
我們好像在當前節點什麼也做不了?
那直接遞迴下去試試
看起來完全就是暴力?
但它其實是好的。
證明
詢問複雜度和一般線段樹都一樣
\(x > snd\)時也時間也不會爆炸
問題在暴力遞迴的部份
證明
假設\(D(p)\)代表節點\(p\)內數字的種類
如果我們需要暴力做
表示有兩種以上的數字\(\geq x\)
然後修改完後他們都會變\(x\)
證明
所以所有節點的\(D(p)\)的和
其實就是當\(ql \leq l \)且\( r \leq qr\)時
還需要暴力操作的上限
如果修改區間完整包含\(p\)
操作後\(D(p)\)至少會\(-1\)
證明
如果不完全包含:
對於這樣的一個區間\(v\)
\(D(v)\)最多也只會\(+1\)
同時這種節點的數量是\(O(logN)\)量級的
增加的複雜度上限:\(O((N+Q)logN)\)
問題.Extreme
同樣的區間問題
但額外加了區間加值的操作
\(N,Q \leq 3 \cdot 10^5\)
d函數感覺會爆炸
一次區間加值
就可能造成數字種類增加\(O(n)\)量級
好像酶辦法做了
其實還是可以!
改一下定義
我們把\(D(p)\)的定義
重新改成以\(p\)為根的子樹中
節點上\(mx\)與\(mx_p\)不同的節點數
區間取min
對一個遞迴終止的節點\(u\)來說
\(u\)上某節點的\(mx\)可能從與\(mx_p\)相同變相異
這樣對所有節點的\(D\)會增加\(O(logN)\)
整筆操作下來\(\Sigma D\)增加的量級是\(O(log^2N)\)
首先遞迴終止的節點會有\(O(logN)\)個
暴力往下
這件事代表我往下把某些
\(mx\)與\(mx_p\)不同的節點改成\(mx_p\)
這其實是消耗\(D(p)\)的
所以單就區間取\(min\)而言
對\(\Sigma D\)的貢獻是\(O(Qlog^2N)\)
區間加值
遞迴終止的節點一樣會有\(O(logN)\)個
每個終止節點的子樹中,\(mx\)都會一起增加
最差的情況就是這\(O(logN)\)個節點都對\(\Sigma D\)有貢獻
另外走下來有一些沒完全覆蓋的節點
增加的\(D\)也是\(O(logN)\)
複雜度
根據剛剛的分析
總複雜度會是\(O(NlogN+Qlog^2N)\)
但聽說跑起來非常快
可能跟一個\(log\)差不多
小結論
其實吉如一線段樹比起特定的資料結構
更算是一種新的概念
我們只是修改了線段樹上遞迴終止的條件
只要確保暴力造成的複雜度可以接受
那就是可以用的解法
感謝聆聽~
進階資結
By Lai Hong An
進階資結
- 280