線段樹合並 總結


今天學習了一下動態開點的線段樹以及線段樹合並吧

理解應該還是比較好理解的,動態開點的話可以避免許多空間的浪費,因為這類問題我們一般建立的是權值線段樹,而權值一般范圍比較大,直接像原來那樣開四倍空間的話空間復雜度不能承受。

動態開點的代碼如下:

void insert(int &i, int l, int r, int x) {
    i = ++T;
    if(l == r) {
        sum[i]++;
        return ;
    }
    int mid = (l + r) >> 1;
    if(x <= mid) insert(lc[i], l, mid, x) ;
    if(x > mid) insert(rc[i], mid + 1, r, x) ;
    update(i);
}

 

因為對應位置的結點所代表的區間范圍都是一樣的,只是保存的信息有所不同,如果信息具有可加性,或者說區間信息可以合並的話,那么就可以兩棵樹同時往根節點開始同時往下遞歸遍歷樹:如果其中一個結點為空,那么我們就返回另外一個結點;否則,選一個結點作為合並之后的點,用另一個點來更新信息即可。最后自底向上維護我們需要的信息就好了。

合並代碼如下:

int merge(int x, int y) {
    if(!x) return y;
    if(!y) return x;
    sum[x] += sum[y] ;//合並區間信息
    lc[x] = merge(lc[x], lc[y]) ;
    rc[x] = merge(rc[x], rc[y]) ;
    return x;//相當於刪除另外一個結點
}

 

假設我們以$n$個點為根建立權值線段樹,由於我們是動態開點,每顆線段樹最后都是一條鏈,那么空間復雜度和時間復雜度都是$O(nlogn)$的。最后我們合並的時候,每次merge操作都會減少一個點,所以最后總的合並過程時間復雜度為$O(nlogn)$,也是十分優秀的了。

 

接下來看幾道例題吧~

 

1.洛谷P3605

題意:

給出一顆樹,每個點都有一個權值,最后對於每個點,輸出在它的子樹中,有多少個點的權值比它大。

 

題解

這是一個比較裸的題,由於權值數量關系是可以合並的,我們對於每一個點建立一顆權值線段樹。之后從1號結點開始dfs,在回溯的過程中不斷合並就行了。

對於每個點,查詢一下目前的線段樹中有多少權值比它大的就可以了。

詳見代碼:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int p[N], a[N], ans[N] ;
int tre[N * 20], lc[N * 20], rc[N * 20], rt[N];
int n, T;
struct Edge{
    int v, next ;
}e[N];
int head[N], tot ;
void adde(int u, int v) {
    e[tot].v = v; e[tot].next = head[u]; head[u] = tot++;
}
void insert(int &i, int l, int r, int x) {
    if(r < l) return ;
    i = ++T;
    if(l == r) {
        tre[i]++ ;
        return ;
    }
    int mid = (l + r) >> 1 ;
    if(x <= mid) insert(lc[i], l, mid, x) ;
    if(x > mid) insert(rc[i], mid + 1, r, x) ;
    tre[i] = tre[lc[i]] + tre[rc[i]] ;
}
int query(int root, int l, int r, int x) {
    if(!root) return 0;
    if(l >= x) return tre[root];
    int ans = 0;
    int mid = (l + r) >> 1;
    if(mid >= x) ans += query(lc[root], l, mid, x) ;
    ans += query(rc[root], mid + 1, r, x) ;
    return ans ;
}
int merge(int x, int y) {
    if(!x) return y;
    if(!y) return x;
    lc[x] = merge(lc[x], lc[y]) ;
    rc[x] = merge(rc[x], rc[y]) ;
    tre[x] = tre[lc[x]] + tre[rc[x]] ;
    return x;
}
void dfs(int u) {
    for(int i = head[u]; i != -1; i = e[i].next) {
        int v = e[i].v ;
        dfs(v) ;
        rt[u] = merge(rt[u], rt[v]) ;
    }
    ans[u] = query(rt[u], 1, n, a[u] + 1) ;
}
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> p[i] , a[i] = p[i];
    sort(p + 1, p + n + 1);
    int D = unique(p + 1, p + n + 1) - p - 1;
    for(int i = 1; i <= n; i++) a[i] = lower_bound(p + 1, p + D + 1, a[i]) - p;
    memset(head, -1, sizeof(head)) ;
    for(int i = 2; i <= n; i++) {
        int x;cin >> x;
        adde(x, i) ;
    }
    for(int i = 1; i <= n; i++) insert(rt[i], 1, n, a[i]) ;
    dfs(1) ;
    for(int i = 1; i <= n; i++) cout << ans[i] << '\n';
    return 0;
}
View Code

 

由於本題中子樹的信息也具有可加性。這個題還可以用樹狀數組來做,記錄一下進點的$tot1$,遍歷完整顆子樹后,查詢現在的$tot2$,這里的$tot1$,$tot2$都為比當前結點權值大的個數,然后$tot2-tot1$即為答案。

代碼如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int p[N], a[N], ans[N] ;
int c[N];
int n, T;
struct Edge{
    int v, next ;
}e[N];
int head[N], tot ;
void adde(int u, int v) {
    e[tot].v = v; e[tot].next = head[u]; head[u] = tot++;
}
int lowbit(int x) {
    return x & (-x) ;
}
void update(int p, int v) {
    for(int i = p ; i < N; i += lowbit(i)) c[i] += v ;
}
int query(int p) {
    int ans = 0 ;
    for(int i = p ; i > 0 ; i -= lowbit(i)) ans += c[i] ;
    return ans ;
}
void dfs(int u) {
    update(a[u], 1);
    int sum1 = query(N - 1) - query(a[u]) ;
    for(int i = head[u]; i != -1; i = e[i].next) {
        int v = e[i].v;
        dfs(v) ;
    }
    int sum2 = query(N - 1) - query(a[u]) ;
    ans[u] = sum2 - sum1 ;
}
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> p[i] , a[i] = p[i];
    sort(p + 1, p + n + 1);
    int D = unique(p + 1, p + n + 1) - p - 1;
    for(int i = 1; i <= n; i++) a[i] = lower_bound(p + 1, p + D + 1, a[i]) - p;
    memset(head, -1, sizeof(head)) ;
    for(int i = 2; i <= n; i++) {
        int x;cin >> x;
        adde(x, i) ;
    }
    dfs(1) ;
    for(int i = 1; i <= n; i++) cout << ans[i] << '\n';
    return 0;
}
View Code

 

2.洛谷P3605

題意:

一開始給出$n$個點,$m$條邊,每個點都有其權值,然后會不斷地加邊,中途可能會有詢問,格式為"$v$ $k$",意義為當前與$v$連通的所有點中,權值第$k$小的島是哪座島。

 

題解:

涉及到連通性,我們考慮用並查集來處理。具體做法為對於每個連通塊建立一顆權值線段樹來維護信息,然后對於加邊過程,就不斷合並兩點所在集合的線段樹就行了。

對於詢問,直接在對應線段樹中詢問當前點所在集合第k小就行。

代碼如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100005;
int n, m;
int v[N], f[N], rt[N], lc[N * 20], rc[N * 20], sum[N * 20], rk[N *20];
int T ;
int find(int x) {
    return f[x] == x ? f[x] : f[x] = find(f[x]) ;
}
void insert(int &i, int l, int r, int x) {
    if(r < l) return ;
    i = ++T;
    if(l == r) {
        sum[i]++;
        return ;
    }
    int mid = (l + r) >> 1;
    if(x <= mid) insert(lc[i], l, mid, x) ;
    if(x > mid) insert(rc[i], mid + 1, r, x) ;
    sum[i] = sum[lc[i]] + sum[rc[i]] ;
}
int merge(int x, int y, int l, int r) {
    if(!x) return y;
    if(!y) return x;
    if(l == r) {
        sum[x] += sum[y] ;
        return x;
    }
    int mid = (l + r) >> 1;
    lc[x] = merge(lc[x], lc[y], l, mid) ;
    rc[x] = merge(rc[x], rc[y], mid + 1, r) ;
    sum[x] = sum[lc[x]] + sum[rc[x]] ;
    return x;
}
int query(int root, int l, int r, int k) {
    if(l == r) return l ;
    int mid = (l + r) >> 1;
    if(sum[lc[root]] >= k) return query(lc[root], l, mid, k) ;
    else return query(rc[root], mid + 1 ,r ,k - sum[lc[root]]) ;
}
int main() {
    scanf("%d%d",&n, &m) ;
    for(int i = 1; i <= n; i++) scanf("%d", &v[i]), rk[v[i]] = i;
    for(int i = 1; i <= n; i++) f[i] = i;
    for(int i = 1; i <= n; i++) insert(rt[i], 1, n, v[i]) ;
    for(int i = 1; i <= m; i++) {
        int u, v;
        scanf("%d%d",&u, &v);
        int fx = find(u), fy = find(v) ;
        if(fx != fy) {
            rt[fx] = merge(rt[fx], rt[fy], 1, n) ;
            f[fy] = fx;
        }
    }
    int q ;
    scanf("%d", &q) ;
    char s[2] ;
    while(q--) {
        int u, v;
        scanf("%s%d%d",s, &u, &v);
        if(s[0] == 'Q') {
            int fx = find(u);
            if(sum[rt[fx]] < v) {
                printf("-1\n");
                continue ;
            }
            int ans = query(rt[fx], 1, n, v) ;
            printf("%d\n", rk[ans]);
        }else {
            int fx = find(u), fy = find(v) ;
            if(fx != fy) {
                rt[fx] = merge(rt[fx], rt[fy], 1, n) ;
                f[fy] = fx;
            }
        }
    }
    return 0;
}
View Code

 

3.洛谷P3521

題意:

給一棵n(1≤n≤200000個葉子的二叉樹,可以交換每個點的左右子樹,要求前序遍歷葉子的逆序對最少。

 

題解:

假設當前點的左右兒子分別為$ls$,$rs$,很容易發現,交換以這兩個結點為根節點的子樹,並不會影響他們的祖宗交換時逆序對的個數。所以我們可以考慮每一層貪心地進行交換,此時局部最優即全局最優。

同時,對於區間$\left[L,R\right]$,設其中點為$mid$,我們只需要考慮這樣的逆序對$\left(x,y\right)$,滿足$L\leq x\leq mid$,$mid+1\leq y\leq R$即可,並不需要考慮在同一個子樹中的逆序對數量。

由於這是權值線段樹,那么逆序對其實很好統計,對於兩顆線段樹代表同一段區間的兩個節點,考慮交換與不交換兩種情況,分別取左、右部分或者右、左部分統計逆序對個數。最后取最小值就可以了。

詳細見代碼:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;
int n ;
ll sum[N * 22] ;
int lc[N * 22], rc[N * 22], rt[N * 22];
int T;
ll ans, sum1, sum2;
int merge(int x, int y) {
    if(!x) return y;
    if(!y) return x;
    sum[x] += sum[y] ;
    sum1 += sum[lc[x]] * sum[rc[y]] ;
    sum2 += sum[rc[x]] * sum[lc[y]] ;
    lc[x] = merge(lc[x], lc[y]) ;
    rc[x] = merge(rc[x], rc[y]) ;
    return x;
}
void insert(int &i, int l, int r, int x) {
    i = ++T;
    if(l == r) {
        sum[i]++;
        return ;
    }
    int mid = (l + r) >> 1;
    if(x <= mid) insert(lc[i], l, mid, x) ;
    if(x > mid) insert(rc[i], mid + 1, r, x) ;
    sum[i] = sum[lc[i]] + sum[rc[i]] ;
}
void dfs(int &p) {
    int x, ls, rs;p = 0;
    cin >> x ;
    if(x == 0) {
        dfs(ls);
        dfs(rs);
        sum1 = sum2 = 0;
        p = ls ;
        p = merge(ls, rs) ;
        ans += min(sum1, sum2) ;
    }
    else insert(rt[x], 1, n, x), p = rt[x];
}
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n;
    int t = 0;
    dfs(t);
    cout << ans ;
    return 0 ;
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM