今天學習了一下動態開點的線段樹以及線段樹合並吧
理解應該還是比較好理解的,動態開點的話可以避免許多空間的浪費,因為這類問題我們一般建立的是權值線段樹,而權值一般范圍比較大,直接像原來那樣開四倍空間的話空間復雜度不能承受。
動態開點的代碼如下:
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; }
由於本題中子樹的信息也具有可加性。這個題還可以用樹狀數組來做,記錄一下進點的$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; }
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; }
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 ; }
