1. 程式人生 > 其它 >資料結構專題-專項訓練:平衡樹

資料結構專題-專項訓練:平衡樹

@

目錄

1. 回顧

在這兩篇博文中:

  1. 平衡樹演算法總結&專題訓練1(無旋平衡樹:替罪羊樹,FHQ Treap)
  2. 平衡樹演算法總結&專題訓練2(有旋平衡樹:AVL 樹,Splay)

我們重點學習了 4 種平衡樹。

當然考慮到在 OI 的實用性以及思維性,我個人認為:

  1. FHQ Treap 和 Splay 一定要掌握!
  2. 替罪羊樹的思想也非常重要,在某些題目當中有重大的作用。
  3. AVL 樹......其實在 OI 中不學也沒有多大問題。

那麼接下來我們看幾道例題,看看平衡樹在實戰中的應用。

2. 例題

題單:

P3850 [TJOI2007]書架

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;
}

P1486 [NOI2004] 鬱悶的出納員

這道題我們首先需要記錄工資變化量 \(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;
}

P2234 [HNOI2002]營業額統計

更簡單。直接找前驅就可以了。

特別需要注意第一天的最小波動值!

還要注意沒有前驅的數!

#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 有時會表現得更好。