【學習筆記】線段樹合併 & 分裂
前置知識:動態開點線段樹
用來解決一些對區間拆分合並的問題。
線段樹合併大概可以替代 DSU,但是常數略大。
對於線段樹分裂合併的空間複雜度問題,一般記憶體要開 \(maxq\times t\times \lceil \log_2maxn \rceil\),其中 \(maxq\) 為詢問次數,\(t\) 為每次詢問的更新操作次數,\(maxn\) 為線段樹值域右端點,有分裂的話還要再 \(\times 2\)。如果 \(1 \le maxq, maxn \le 10^5\) 的話,記憶體大概要開 \(maxn \times 40\)。
動態開點
int newnode(){
if(delcnt) return cache[delcnt--];
return ++cnt;
}
void del(int &x){
cache[++delcnt] = x;
ls = rs = sum[x] = 0;
}
修改操作時需要傳取地址符並在不存在的 \(x\) 處新建節點,這樣子遞迴回去的時候左右兒子就建好了。
不需要的時候刪除節點可以將其存在一個陣列裡,要新建節點的時候從這裡面取可以省空間。
例如一個最簡單的單點修改:
void update(int &x, int l, int r, int p, int v){
if(!x) x = newnode();
sum[x] += v;
if(l == r)
return;
if(p<=mid) update(lson, p, v);
else update(rson, p, v);
}
線段樹合併
線段樹合併的目的是把兩棵權值線段樹合併。
相當於就是把對應的節點的權值相加。即將線段樹 B 的節點權值加到線段樹 A 上,這樣子線段樹 B 就可以不用了。
對應程式碼:
int merge(int &x, int &y){
if(!x || !y) return x+y;
son[x][0] = merge(son[x][0], son[y][0]);
son[x][1] = merge(son[x][1], son[y][1]);
sum[x] += sum[y];
del(y);
return x;
}
線段樹分裂
線段樹分裂的目的是把一棵權值線段樹分裂成兩顆權值線段樹。
分裂有按權值(第 \(k\) 小)分裂和按區間分裂。
按權值(第 \(k\) 小)分裂
設 \(v\) 為 \(x\) 的左子樹權值的大小。
- 當 \(v=k\) 時,
swap
後直接返回。 - 當 \(v>k\) 時,右邊可以全歸 \(y\),此時
swap
後還需要遞迴分裂左子樹,因為中間還有一段沒有歸到右邊。 - 當 \(v<k\) 時,遞迴分裂右子樹。
記得更新權值。
對應程式碼:
void split(int &x, int l, int r, int &y, int k){ // 分前 k 小,即 1~k-1 和 k~n
if(sum[ls]==k){
swap(son[x][1], son[y][1]);
return;
}
if(!y) y = newnode();
if(sum[ls]>=k){
swap(son[x][1], son[y][1]);
split(lson, son[y][0], k);
}
else
split(rson, son[y][1], k-sum[ls]);
sum[y] = sum[son[y][0]]+sum[son[y][1]];
sum[x] -= sum[y];
}
按區間分裂
像普通線段樹一樣遞迴,如果當前區間被查詢區間包含,則 swap
後返回。
對應程式碼:
void split(int &x, int l, int r, int &y, int ql, int qr){
if(ql<=l && r<=qr){
swap(x, y);
// 將 x 存到 y,相當於 y = x, del(x)
return;
}
if(!y) y = newnode();
if(ql<=mid) split(lson, son[y][0], ql, qr);
if(qr>mid) split(rson, son[y][1], ql, qr);
sum[y] = sum[son[y][0]]+sum[son[y][1]];
sum[x] -= sum[y];
// sum[x] 也可以寫成左右子樹相加形式的
}
注意新建 \(y\) 節點都要在遞迴邊界之後開,這樣遞迴邊界的交換才能使 \(x\) 的這部分為空。
P5494 【模板】線段樹分裂
模板題。
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
constexpr int N = 2e5+5;
struct segtree{
int son[N*35][2], rt[N], cnt, rtcnt=1;
ll sum[N*35];
int cache[N*35], delcnt;
#define ls (son[x][0])
#define rs (son[x][1])
#define mid (((l)+(r))>>1)
#define lson ls, l, mid
#define rson rs, mid+1, r
int newnode(){
if(delcnt) return cache[delcnt--];
return ++cnt;
}
void del(int &x){
cache[++delcnt] = x;
ls = rs = sum[x] = 0;
}
void update(int &x, int l, int r, int p, int v){
if(!x) x = newnode();
sum[x] += v;
if(l == r)
return;
if(p<=mid) update(lson, p, v);
else update(rson, p, v);
}
ll query(int x, int l, int r, int ql, int qr){
if(ql<=l && r<=qr)
return sum[x];
ll res = 0;
if(ql<=mid) res += query(lson, ql, qr);
if(qr>mid) res += query(rson, ql, qr);
return res;
}
int querykth(int x, int l, int r, int k){
if(k > sum[x]) return -1;
if(l == r)
return l;
if(sum[ls]>=k) return querykth(lson, k);
else return querykth(rson, k-sum[ls]);
}
int merge(int &x, int &y){
if(!x || !y) return x+y;
son[x][0] = merge(son[x][0], son[y][0]);
son[x][1] = merge(son[x][1], son[y][1]);
sum[x] += sum[y];
del(y);
return x;
}
void split(int &x, int l, int r, int &y, int ql, int qr){
if(ql<=l && r<=qr){
swap(x, y);
// 將 x 存到 y,相當於 y = x, del(x)
return;
}
if(!y) y = newnode();
if(ql<=mid) split(lson, son[y][0], ql, qr);
if(qr>mid) split(rson, son[y][1], ql, qr);
sum[y] = sum[son[y][0]]+sum[son[y][1]];
sum[x] -= sum[y];
// sum[x] 也可以寫成左右子樹相加形式的
}
void split(int &x, int l, int r, int &y, int k){ // 分前 k 小,即 1~k-1 和 k~n
if(sum[ls]==k){
swap(son[x][1], son[y][1]);
return;
}
if(!y) y = newnode();
if(sum[ls]>=k){
swap(son[x][1], son[y][1]);
split(lson, son[y][0], k);
}
else
split(rson, son[y][1], k-sum[ls]);
sum[y] = sum[son[y][0]]+sum[son[y][1]];
sum[x] -= sum[y];
}
}T;
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
int x; cin>>x;
T.update(T.rt[1], 1, n, i, x);
}
for(int i=1; i<=m; i++){
int op; cin>>op;
if(op == 0){
int p, x, y; cin>>p>>x>>y;
T.split(T.rt[p], 1, n, T.rt[++T.rtcnt], x, y);
} else if(op == 1){
int p, t; cin>>p>>t;
T.merge(T.rt[p], T.rt[t]);
} else if(op == 2){
int p, x, q; cin>>p>>x>>q;
T.update(T.rt[p], 1, n, q, x);
} else if(op == 3){
int p, x, y; cin>>p>>x>>y;
cout<<T.query(T.rt[p], 1, n, x, y)<<"\n";
} else if(op == 4){
int p, k; cin>>p>>k;
cout<<T.querykth(T.rt[p], 1, n, k)<<"\n";
}
}
return 0;
}
P4556 [Vani有約會] 雨天的尾巴 /【模板】線段樹合併
原圖就是一棵樹,對於這棵樹上的每個節點有一個桶記錄對應種類的新增次數,用權值線段樹維護這個桶。
每次詢問對於 \(u \rightarrow v\) 上的節點的桶的位置 \(z\) 需要 \(+1\),暴力的話需要在每個節點都更新一次線段樹。考慮樹上差分,即在 \(u, v\) 處 \(+1\),在 \(lca(u, v), fa_{lca(u, v)}\) 處 \(-1\),最後向上更新(線段樹合併)統計答案。
每個節點的線段樹需要記錄最大值和最大值所在位置。
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 1e5+5;
int fa[N][19], dep[N];
vector<int> g[N];
int ans[N], R;
struct segtree{
int ls[N*70], rs[N*70], rt[N], cache[N*70], cnt, delcnt;
int mx[N*70], id[N*70];
#define mid ((l)+(r)>>1)
#define lson ls[x], l, mid
#define rson rs[x], mid+1, r
int newnode(){
if(delcnt) return cache[delcnt--];
return ++cnt;
}
void del(int &x){
cache[++delcnt] = x;
ls[x] = rs[x] = mx[x] = id[x] = 0;
}
void pushup(int x){
if(mx[ls[x]]>=mx[rs[x]]) mx[x] = mx[ls[x]], id[x] = id[ls[x]];
else mx[x] = mx[rs[x]], id[x] = id[rs[x]];
}
void update(int &x, int l, int r, int p, int v){
if(!x) x = newnode();
if(l == r){
mx[x] += v; id[x] = l;
return;
}
if(p<=mid) update(lson, p, v);
else update(rson, p, v);
pushup(x);
}
int merge(int &x, int l, int r, int &y){
if(!x || !y) return x+y;
if(l == r){
mx[x] += mx[y];
del(y);
return x;
}
if(!y) y = newnode();
ls[x] = merge(lson, ls[y]);
rs[x] = merge(rson, rs[y]);
pushup(x);
del(y);
return x;
}
}T;
void dfs(int u, int f){
dep[u] = dep[f]+1; fa[u][0] = f;
for(int i=1; i<19; i++) fa[u][i] = fa[fa[u][i-1]][i-1];
for(int v : g[u]) if(v != f) dfs(v, u);
}
int LCA(int u, int v){
if(dep[u] > dep[v]) swap(u, v);
for(int d=dep[v]-dep[u], i=0; d; i++, d>>=1) if(d&1) v = fa[v][i];
if(u == v) return u;
for(int i=18; i>=0; i--) if(fa[u][i]!=fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
void redfs(int u, int f){
if(!T.rt[u]) T.rt[u] = T.newnode(); // 不存在要開,不然 merge 會直接 return
for(int v : g[u]){
if(v != f){
redfs(v, u);
T.merge(T.rt[u], 1, R, T.rt[v]);
}
}
ans[u] = T.id[T.rt[u]];
if(T.mx[T.rt[u]]==0) ans[u] = 0;
//cout<<u<<" "<<T.rt[u]<<" "<<T.mx[T.rt[u]]<<" "<<T.id[T.rt[u]]<<"\n";
}
struct node{
int x, y, z, lca;
}op[N];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<n; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
for(int i=1; i<=m; i++){
cin>>op[i].x>>op[i].y>>op[i].z;
op[i].lca = LCA(op[i].x, op[i].y);
R = max(R, op[i].z);
}
for(int i=1; i<=m; i++){
T.update(T.rt[op[i].x], 1, R, op[i].z, 1);
T.update(T.rt[op[i].y], 1, R, op[i].z, 1);
T.update(T.rt[op[i].lca], 1, R, op[i].z, -1);
T.update(T.rt[fa[op[i].lca][0]], 1, R, op[i].z, -1);
}
redfs(1, 0);
for(int i=1; i<=n; i++)
cout<<ans[i]<<"\n";
return 0;
}
P1600 [NOIP2016 提高組] 天天愛跑步
因為對一個點從 \(u\) 到 \(v\) 的模擬每一時刻的貢獻複雜度肯定不優,我們倒過來考慮對於每個觀察員 \(i\),哪些運動點會在 \(w[i]\) 時刻對其作貢獻。
記 \(lca\) 為 \(u\) 和 \(v\) 的最近公共祖先,將一個點從 \(u\) 到 \(v\) 分為 \(u\) 到 \(lca\) 的上行段和 \(lca\) 到 \(v\) 的下行段。
先分析上行段:
對於 \(u\) 到 \(lca\) 路徑上的任意一點 \(k\),從 \(u\) 到 \(k\) 需要的時間 \(t=dep_u-dep_k\)。所以這個點上的觀察員可以觀測到這位玩家的充要條件是 \(w_k=t\),即 \(dep_k+w_k=dep_u\)。
考慮對每個節點維護一個桶,對路徑上每個節點的桶的 \(dep_u\) 對應的位置加上 \(1\),最後查詢 \(dep_k+w_k\) 上的數量即可。可以樹上差分,將 \(u\) 上的桶的 \(dep_u\) 對應的位置加上 \(1\),將 \(fa_{lca}\) 上的桶的 \(dep_u\) 對應的位置減 \(1\)。 最後統計答案從葉子節點向上合併即可。(當然因為是區間合併所以要上線段樹合併)
接著分析下行段:
對於 \(lca\) 到 \(v\) 路徑上的任意一點 \(k\),從 \(lca\) 到 \(k\) 需要的時間 \(t_1=dep_k-dep_{lca}\),從 \(u\) 到 \(lca\) 需要的時間 \(t_2 = dep_u-dep_{lca}\),所以從 \(u\) 到 \(v\) 需要的時間 \(t_3 = t_1+t_2 = dep_u+dep_k-2\times dep_{lca}\)。所以這個點上的觀察員可以觀測到這位玩家的充要條件是 \(w_k=t_3\),即 \(dep_k-w_k=2\times dep_{lca}-dep_u\)。
同樣的,將 \(v\) 上的桶的 \(2\times dep_{lca}-dep_u\) 對應的位置加 \(1\),將 \(lca\) 上的桶的 \(2\times dep_{lca}-dep_u\) 對應的位置減 \(1\)(抵消上行段的重複計算)。最後查詢 \(dep_k-w_k\) 上的數量。
總貢獻即為線段樹上的兩點的值的和。要特判 \(w_u = 0\) 的情況,因為此時會重複算兩次。
注意線段樹值域要開兩倍,因為線段樹查詢位置 \(dep_u+w_u \le 2\times n\)。
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
constexpr int N = 3e5+5;
int n, m, _n;
vector<int> g[N];
int dep[N], fa[N][20];
void dfs(int u, int f){
dep[u] = dep[f]+1; fa[u][0] = f;
for(int i=1; i<=__lg(n); i++) fa[u][i] = fa[fa[u][i-1]][i-1];
for(int v : g[u]) if(v != f) dfs(v, u);
}
int LCA(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
for(int d=dep[u]-dep[v], i=0; d; i++, d>>=1) if(d&1) u = fa[u][i];
if(u == v) return u;
for(int i=__lg(n); i>=0; i--) if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
struct segtree{
int rt[N], ls[N*80], rs[N*80], cache[N*80], delcnt, cnt;
int sum[N*80];
#define mid (((l)+(r))>>1)
#define lson ls[x], l, mid
#define rson rs[x], mid+1, r
int newnode(){
if(delcnt) return cache[delcnt--];
return ++cnt;
}
void del(int x){
cache[++cnt] = x;
sum[x] = ls[x] = rs[x] = 0;
}
void pushup(int x){sum[x] = sum[ls[x]]+sum[rs[x]];}
void update(int &x, int l, int r, int p, int v){
if(!x) x = newnode();
if(l == r){
sum[x] += v;
return;
}
if(p<=mid) update(lson, p, v);
else update(rson, p, v);
pushup(x);
}
int query(int x, int l, int r, int p){
if(l == r)
return sum[x];
if(p<=mid) return query(lson, p);
else return query(rson, p);
}
int merge(int &x, int &y){
if(!x || !y) return x+y;
if(!y) y = newnode();
ls[x] = merge(ls[x], ls[y]);
rs[x] = merge(rs[x], rs[y]);
sum[x] += sum[y];
del(y);
return x;
}
}T;
int w[N], ans[N];
void redfs(int u, int f){
if(!T.rt[u]) T.rt[u] = T.newnode();
for(int v : g[u]){
if(v == f) continue;
redfs(v, u);
T.merge(T.rt[u], T.rt[v]);
}
ans[u] = T.query(T.rt[u], -_n, _n, dep[u]+w[u]) + T.query(T.rt[u], -_n, _n, dep[u]-w[u]);
if(w[u] == 0) ans[u] /= 2;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m; _n = n*2;
for(int i=1; i<n; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1; i<=n; i++)
cin>>w[i];
dfs(1, 0);
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
int lca = LCA(u, v);
T.update(T.rt[u], -_n, _n, dep[u], 1);
T.update(T.rt[v], -_n, _n, 2*dep[lca]-dep[u], 1);
T.update(T.rt[lca], -_n, _n, 2*dep[lca]-dep[u], -1); // dep[u] 也可
T.update(T.rt[fa[lca][0]], -_n, _n, dep[u], -1);
}
redfs(1, 0);
for(int i=1; i<=n; i++)
cout<<ans[i]<<" ";
return 0;
}
參考資料
- 題解【模板】線段樹分裂 - ix35