資料結構專題-專項訓練:平衡樹
阿新 • • 發佈:2022-04-07
@
目錄1. 回顧
在這兩篇博文中:
我們重點學習了 4 種平衡樹。
當然考慮到在 OI 的實用性以及思維性,我個人認為:
- FHQ Treap 和 Splay 一定要掌握!
- 替罪羊樹的思想也非常重要,在某些題目當中有重大的作用。
- AVL 樹......其實在 OI 中不學也沒有多大問題。
那麼接下來我們看幾道例題,看看平衡樹在實戰中的應用。
2. 例題
題單:
FHQ Treap 按照大小分裂的模板題,直接做就可以了。
更具體的,在第 \(i\) 個位置插入字串 \(s\) 的時候,我們將樹按照 \(i - 1\) 的大小分裂,然後合併即可。
查詢?也差不多。
程式碼:
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int MAXN = 2e5 + 10; int n, m, cnt, root, q; struct node { string val; int l, r, key, size; }tree[MAXN]; int read() { int sum = 0, fh = 1; char ch = getchar(); while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();} while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();} return sum * fh; } void update(int now) {tree[now].size = tree[tree[now].l].size + tree[tree[now].r].size + 1;} int Make_Node(string val) { int now = ++cnt; tree[cnt].val = val; tree[cnt].key = rand(); tree[cnt].size = 1; return now; } void split(int now, int val, int &x, int &y) { if (now == 0) x = y = 0; else { if (tree[tree[now].l].size < val) { x = now; split(tree[now].r, val - tree[tree[now].l].size - 1, tree[now].r, y); } else { y = now; split(tree[now].l, val, x, tree[now].l); } update(now); } } int merge(int x, int y) { if (!x || !y) return x + y; if (tree[x].key < tree[y].key) { tree[x].r = merge(tree[x].r, y); update(x); return x; } else { tree[y].l = merge(x, tree[y].l); update(y); return y; } } void Insert(int pos, string val) { int x, y; split(root, pos - 1, x, y); root = merge(merge(x, Make_Node(val)), y); } string ask(int pos) { int x, y, z; split(root, pos, y, z); split(y, pos - 1, x, y); string str = tree[y].val; root = merge(merge(x, y), z); return str; } int main() { srand(time(0)); n = read(); for (int i = 1; i <= n; ++i) { string str; cin >> str; Insert(i, str); } m = read(); for (int i = 1; i <= m; ++i) { string str; int pos; cin >> str; pos = read(); Insert(pos + 1, str); } q = read(); for (int i = 1; i <= q; ++i) { int pos = read(); cout << ask(pos + 1) << "\n"; } return 0; }
這道題我們首先需要記錄工資變化量 \(delta\)。
使用 FHQ Treap。
插入操作:
老生常談,注意初始工資要減去 \(delta\),然後插入。不要忘記特判。
加減工資:
直接修改 \(delta\) 即可。
找第 \(k\) 大:
老生常談。注意可能第 \(k\) 大不存在。
注意:在每一次操作後,我們都需要將工資小於 \(min - delta\)(\(min\) 為初始工資下界)的人刪除,這個直接 FHQ Treap 分裂出來捨棄掉就好了。
程式碼:
#include <bits/stdc++.h> using namespace std; const int MAXN = 3e5 + 10; int n, minn, cnt, root, ans, delta; struct node { int l, r, val, size, key; }tree[MAXN]; int read() { int sum = 0, fh = 1; char ch = getchar(); while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();} while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();} return sum * fh; } int Make_Node(int val) { ++cnt; tree[cnt].val = val; tree[cnt].size = 1; tree[cnt].key = rand(); return cnt; } void update(int now) {tree[now].size = tree[tree[now].l].size + tree[tree[now].r].size + 1;} void split(int now, int val, int &x, int &y) { if (!now) x = y = 0; else { if (tree[now].val <= val) { x = now; split(tree[now].r, val, tree[now].r, y); } else { y = now; split(tree[now].l, val, x, tree[now].l); } update(now); } } int merge(int x, int y) { if (!x || !y) return x + y; else { if (tree[x].key > tree[y].key) { tree[x].r = merge(tree[x].r, y); update(x); return x; } else { tree[y].l = merge(x, tree[y].l); update(y); return y; } } } void Insert(int val) { int x, y; split(root, val, x, y); root = merge(merge(x, Make_Node(val)), y); } void Find_kth(int val) { int now = root; while (now) { if (tree[tree[now].r].size + 1 == val) break; if (tree[tree[now].r].size >= val) now = tree[now].r; else {val -= tree[tree[now].r].size + 1; now = tree[now].l;} } printf("%d\n", tree[now].val + delta); } void deal(int val) { int x, y; split(root, val - 1, x, y); ans += tree[x].size; root = y; } int main() { srand(time(0)); n = read(); minn = read(); for (int i = 1; i <= n; ++i) { char ch; cin >> ch; int k = read(); switch(ch) { case 'I': if (k < minn) break; Insert(k - delta); break; case 'A': delta += k; break; case 'S': delta -= k; break; case 'F': if (k > cnt - ans) printf("-1\n"); else Find_kth(k); break; } deal(minn - delta); } printf("%d\n", ans); return 0; }
更簡單。直接找前驅就可以了。
特別需要注意第一天的最小波動值!
還要注意沒有前驅的數!
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 32767 + 10;
int n, cnt, root;
LL ans;
struct node
{
int l, r, size, val, key;
}tree[MAXN];
LL read()
{
LL sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
int Make_Node(int val)
{
++cnt;
tree[cnt].size = 1;
tree[cnt].val = val;
tree[cnt].key = rand();
return cnt;
}
void update(int now) {tree[now].size = tree[tree[now].l].size + tree[tree[now].r].size + 1;}
void split(int now, int val, int &x, int &y)
{
if (!now) x = y = 0;
else
{
if (tree[now].val <= val) {x = now; split(tree[now].r, val, tree[now].r, y);}
else {y = now; split(tree[now].l, val, x, tree[now].l);}
update(now);
}
}
int merge(int x, int y)
{
if (!x || !y) return x + y;
if (tree[x].key > tree[y].key)
{
tree[x].r = merge(tree[x].r, y);
update(x); return x;
}
else
{
tree[y].l = merge(x, tree[y].l);
update(y); return y;
}
}
void Insert(int val)
{
int x, y; split(root, val, x, y);
root = merge(merge(x, Make_Node(val)), y);
}
int main()
{
srand(time(0));
n = read(); int t = read();
ans = t; Insert(t);
for (int i = 2; i <= n; ++i)
{
t = read();
Insert(t);
int x, y, z, sum = 0x7f7f7f7f;
split(root, t - 1, x, y);
split(y, t, y, z);
if (tree[y].size > 1) {root = merge(merge(x, y), z); continue;}
else
{
if (x != 0)
{
int now = x;
while (tree[now].r) now = tree[now].r;
sum = min(sum, abs(tree[now].val - t));
}
if (z != 0)
{
int now = z;
while (tree[now].l) now = tree[now].l;
sum = min(sum, abs(tree[now].val - t));
}
}
ans += sum;
root = merge(merge(x, y), z);
}
printf("%lld\n", ans);
return 0;
}
3. 總結
或許您已經發現了,這幾道題都是針對單點操作的。
的確,平衡樹的入門題都是針對單點操作,而後面的題目涉及到區間操作時就要使用線段樹的 lazy_tag-懶標記 思想了。
具體後面再看。
為什麼都是 FHQ Treap?因為它碼量短啊。
當然區間問題 Splay 有時會表現得更好。