【學習筆記】線段樹合併 & 分裂

FlyPancake發表於2024-11-20

【學習筆記】線段樹合併 & 分裂

前置知識:動態開點線段樹

用來解決一些對區間拆分合並的問題。

線段樹合併大概可以替代 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

相關文章