筆記:左偏樹
摘要:賀了三道題,啥也沒學會。
賀的是:hsfzLZH1 和 05年集訓隊論文 和 OI-wiki
左偏樹的定義和性質
我們定義一個 外節點 的 \(dis\) 是 1。每個點的 \(dis\) 等於它到最近的外節點的距離。特別地,空節點的 \(dis\) 是 -1。
那麼,對於一個 \(n\) 個節點的二叉樹,根的 \(dis\) 不超過 \(\log {(n+1)}-1\)。
Proof
一個根節點 \(dis\) 為 \(x\) 的二叉樹,他至少有 \(x\) 層是滿的。
\(n\ge 2^{x+1}-1\)
\(x\le \log {(n+1)}-1\)
左偏樹,是一棵滿足如下兩個性質的二叉樹。
兩個性質:
- 具有左偏性質。即:\(\forall u\in T,dis(lson(u))\ge dis(rson(u))\)
- 具有堆性質。
兩個結論:
-
\(dis(u)=dis(rson(u))+1\)。下圖是論文裡的示例圖。
- 往右一直走,最多走 \(\log {(n+1)}-1\) 步
原因:對於一個 \(n\) 個節點的二叉樹,根的 \(dis\) 不超過 \(\log {(n+1)}-1\)。
左偏樹的操作
主要是合併,插入,刪除根,建樹,刪除任意節點。
這裡以小根堆為例。
合併
合併兩棵左偏樹。
把權值較大的左偏樹 \(B\) 併到 根權值較小的的左偏樹 \(A\) 的右子樹 \(right(A)\)
合併完了,這時候可能不再滿足 \(dis(left(A))\ge dis(right(A))\),這時就交換 \(A\) 的左右子樹。
(下圖中第一次交換是為了保證 \(A\) 的根 比 \(B\) 的根 權值小)
時間複雜度:每次向右走一步。\(O(\log n)\)
插入
直接將單點當做一棵樹,執行合併操作。\(O(\log n)\)
刪除根
合併倆兒子。\(O(\log n)\)
建樹
下面都是論文的解釋。
刪除任意節點
下面都是OI wiki。
先將左右兒子合併,然後自底向上更新 、不滿足左偏性質時交換左右兒子,當 \(dist\) 無需更新時結束遞迴:
int& rs(int x) { return t[x].ch[t[t[x].ch[1]].d < t[t[x].ch[0]].d]; } // 有了 pushup,直接 merge 左右兒子就實現了刪除節點並保持左偏性質 int merge(int x, int y) { if (!x || !y) return x | y; if (t[x].val < t[y].val) swap(x, y); t[rs(x) = merge(rs(x), y)].fa = x; pushup(x); return x; } void pushup(int x) { if (!x) return; if (t[x].d != t[rs(x)].d + 1) { t[x].d = t[rs(x)].d + 1; pushup(t[x].fa); } }
習題
-
這真的適合當板題嗎。。逐漸不會並查集的路徑壓縮。
code
#include<bits/stdc++.h> using namespace std; typedef long long ll; #define fi first #define se second #define mkp make_pair #define PII pair<int,int> const int N = 1e5 + 10; int n, m, f[N], val[N], dis[N], tr[N][2]; int find(int x) { return (x == f[x]) ? x : f[x] = find(f[x]); } int merge(int x, int y) { if(!x || !y) return x | y; if(val[x] > val[y] || (val[x] == val[y] && x > y)) swap(x, y); tr[x][1] = merge(tr[x][1], y); if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]); f[tr[x][0]] = f[tr[x][1]] = x; //最多也是跳log次。 dis[x] = dis[tr[x][1]] + 1; return x; } void del(int x) { val[x] = -1; f[tr[x][0]] = tr[x][0]; f[tr[x][1]] = tr[x][1]; f[x] = merge(tr[x][0], tr[x][1]); //子樹中有些點路徑壓縮到的還是x,讓他們跳到正確的根。 } int main() { dis[0] = -1; scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) scanf("%d", &val[i]), f[i] = i; while(m--) { int op, x, y; scanf("%d", &op); if(op == 1) { scanf("%d%d", &x, &y); if(val[x] == -1 || val[y] == -1) continue; x = find(x); y = find(y); if(x != y) f[x] = f[y] = merge(x, y); } else { scanf("%d", &x); if(val[x] == -1) { puts("-1"); continue; } int y = find(x); printf("%d\n", val[y]); del(y); } } return 0; }
-
是誰在賀自己以前的程式碼?是我啊,那沒事了。
題意:給你一棵樹(保證 \(fa_i<i\)),每個節點有val,lead。對於一個點,在它子樹裡選 \(\sum val \le m\) 的點,\(ans=max(ans,lead*tot)\) 。求最大ans
題解:
每個點維護一個堆,表示當前它子樹中價效比最高且 \(\sum val \le m\) 的點。
從下到上遍歷樹(這裡就直接從 \(n\) 掃到 \(1\)) 每次把該點的子樹和它父親當前子樹合併。
每個點和父親合併一次,被刪除至多一次。\(O(\log n)\)
code
#include<bits/stdc++.h> using namespace std; typedef long long ll; #define fi first #define se second #define mkp make_pair #define PII pair<int,int> const int N = 1e5 + 10; int n, m, fa[N], tot[N], val[N], lead[N], dis[N], tr[N][2], rt[N]; ll sum[N]; int merge(int x, int y) { if(!x || !y) return x | y; if(val[x] < val[y]) swap(x, y); tr[x][1] = merge(tr[x][1], y); if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]); dis[x] = dis[tr[x][1]] + 1; return x; } int main() { dis[0] = -1; scanf("%d%d", &n, &m); ll ans = 0; for(int i = 1; i <= n; i++) { scanf("%d%d%d", &fa[i], &val[i], &lead[i]); rt[i] = i; sum[i] = val[i]; tot[i] = 1; ans = max(ans, 1ll * lead[i]); } for(int i = n; i > 1; i--) { sum[fa[i]] += sum[i]; tot[fa[i]] += tot[i]; rt[fa[i]] = merge(rt[fa[i]], rt[i]); while(sum[fa[i]] > m) { sum[fa[i]] -= val[rt[fa[i]]]; tot[fa[i]]--; rt[fa[i]] = merge(tr[rt[fa[i]]][0], tr[rt[fa[i]]][1]); } ans = max(ans, 1ll * lead[fa[i]] * tot[fa[i]]); } printf("%lld\n", ans); return 0; }
-
論文題/yun
首先考慮將嚴格遞增改成單調不降,\(b[i]-i\) 是單調不降序列。
不妨以 \(a[i]-i\) 作為 \(a\) 求單調不降的 \(b\),最後答案每個 \(+i\)
考慮特殊情況:
- 情況1:\(a\) 單調不降:\(a=b\)
- 情況2:\(a\) 單調不增:\(b_i\) 都是 \(a\) 中位數
考慮將原序列分成 \(m\) 段,每一段都是 情況2。(情況1 可以分裂成很多 情況2)
我們希望 \(b\) 遞增,如果有相鄰兩段不遞增,那麼這一整段我們都取原來 \(a\) 數組裡這一整段的中位數。Proof
設 \(a_{1}\) 到 \(a_n\) 中位數 為 \(u\),\(a_{n+1}\) 到 \(a_m\) 中位數是 \(v\),\(a_{1}\) 到 \(a_m\) 中位數是 \(w\)
現在的 \(b\) 是 \((u,u,...,u,v,v,...v)\)
如果 \(u\le v\),那很顯然不用動了
如果 \(u>v\),我們要證明不存在答案比 \((w,w,w...w)\) 更優的 \(b\)
/ll 看論文吧。現在問題變成了:合併兩個有序集以及查詢某個有序集內的中位數。
我們發現,只有當某一區間內的中位數比後一區間內的中位數大時,合併操作才會發生,也就是說,任一區間與後面的區間合併後,該區間內的中位數不會變大。
於是我們可以用最大堆來維護每個區間內的中位數,當堆中的元素大於該區間內元素的一半時,刪除堆頂元素,這樣堆中的元素始終為區間內較小的一半元素,堆頂元素即為該區間內的中位數。
——論文
我們維護的堆,是前 \(\lceil \frac {len} 2\rceil\) 大的值。
使用左偏樹,查詢 \(O(1)\) ,合併 \(O(\log n)\)。 可以達到 \(O(n\log n)\) 的複雜度。code
#include<bits/stdc++.h> using namespace std; typedef long long ll; #define fi first #define se second #define mkp make_pair #define PII pair<int,int> const int N = 1e6 + 10; int n, m, dis[N], tr[N][2], val[N], a[N], b[N]; int top; struct node { int rt, l, r, sz, mid; node() { rt = 0; l = 0; r = 0; sz = 0; mid = 0; } node(int smrt, int sml, int smr, int smsz, int smmid) { rt = smrt; l = sml; r = smr; sz = smsz; mid = smmid; } }st[N]; int merge(int x, int y) { if(!x || !y) return x | y; if(val[x] < val[y]) swap(x, y); tr[x][1] = merge(tr[x][1], y); if(dis[tr[x][0]] < dis[tr[x][1]]) swap(tr[x][0], tr[x][1]); dis[x] = dis[tr[x][1]] + 1; return x; } int main() { dis[0] = -1; scanf("%d", &n); for(int i = 1; i <= n; i++) { scanf("%d", &a[i]); val[i] = a[i] - i; } for(int i = 1; i <= n; i++) { st[++top] = node(i, i, i, 1, a[i] - i); while(top > 1 && st[top - 1].mid > st[top].mid) { top--; st[top].r = st[top + 1].r; st[top].sz += st[top + 1].sz; st[top].rt = merge(st[top].rt, st[top + 1].rt); while(st[top].sz > (st[top].r - st[top].l + 2) / 2) { st[top].sz--; st[top].rt = merge(tr[st[top].rt][0], tr[st[top].rt][1]); } st[top].mid = val[st[top].rt]; } } for(int i = 1; i <= top; i++) for(int j = st[i].l; j <= st[i].r; j++) b[j] = st[i].mid + j; ll ans = 0; for(int i = 1; i <= n; i++) ans += abs(b[i] - a[i]); printf("%lld\n", ans); for(int i = 1; i <= n; i++) printf("%d ", b[i]); puts(""); return 0; } /* 5 2 5 46 12 1 */