平衡树学习笔记

不持续更新。

1 FHQ-Treap#

1.1 前置知识#

BST

Heap

FHQ-Treap 一般使用小根堆。

1.2 FHQ-Treap 简述#

FHQ-Treap 是一种基于分裂和合并操作的平衡树。它没有旋转,极易上手,非常适合 cainiaoshanglu

1.3 FHQ-Treap 核心思想#

我们对于一个点存储两个权值 ti,ai, 其中 ai 满足小根堆性质,ti 满足 BST 性质。 我们可以对于 ai 进行随机赋值, 使得期望时间复杂度为 O(logn) 的.

FHQ-Treap 基于合并与分裂函数, 轻易的实现了 P3369 的六种功能, 即易懂又他妈的好写.

1.4 FHQ-Treap 基础操作#

1.4.1 更新操作#

用于更新节点信息改变后节点的值。

void pushUp(int x) {
    t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
}

1.4.2 合并操作#

满足 ai,即小根堆来合并.

发现由于之前将 p,q 分裂开了,所以 q 里面的所有值都大于 p 的。也就是说,我们只需要确定父子关系即可合并。

假设现在两棵树合并到 x,y.

  • 若当前 ax<ay,则显然 yx 的右儿子。

  • 若当前 ax>ay,则显然 xy 的左儿子。

递归合并即可。

int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (t[x].a < t[y].a) {
        t[x].ch[1] = merge(t[x].ch[1], y);
        pushUp(x);
        return x;
    } else {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        pushUp(y);
        return y;
    } 
}

实现细节:注意,这里的合并函数是默认了 x 点子树的所有权值小于等于 y 点子树的。

1.4.3 分裂操作#

我们通过 tx,即 BST 来分裂。

假设我们要把 p 的分裂开来,那么假设现在走到 x

  • 如果 txp,那么将 x 及其右子树全部连到左树上,继续递归左儿子。

  • 如果 tx<p,那么将 x 及其左子树全部连到右树上,继续递归右儿子。

一般有两种分裂方式,一种是 按权值 tx,一种是 按子树大小 sizx。这里两种都放一下。

按照权值分裂:

void split(int now, int k, int &x, int &y) {
    if (now == 0) return x = y = 0, void();
    if (t[now].t >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
    else               y = now, split(t[now].ch[1], k, x, t[now].ch[1]);
    pushUp(now);
}

按照大小分裂:

void split(int now, int k, int &x, int &y) {
    if (now == 0) return x = y = 0, void();
    if (k <= t[t[now].ch[0]].siz) x = now, split(t[now].ch[0], k, t[x].ch[0], y);
    else                          y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[x].ch[1]);
    pushUp(now);
}

1.5 FHQ-Treap 复合操作#

1.5.1 插入操作#

假设插入的权值为 v

我们先按照权值对 FHQ-Treap 进行分裂,把平衡树分成 v<v 的两部分。最后分别与新建点合并即可。

void Insert(int key) {
    int p, q;
    split(rt, key, p, q);
    p = merge(newNode(key), p);
    rt = merge(q, p);
}

1.5.2 删除操作#

我们考虑把平衡树分裂成 v<v 的两部分。

对于 v 的部分,我们再把他分裂成 >v=v 的两部分。

对于 =v 的部分,我们可以删除它的根 —— 把它的两个儿子合并。最后全部合在一起即可。

void Delete(int key) {
    int p, q, o;
    split(rt, key, p, q);
    split(p, key + 1, p, o);
    o = merge(t[o].ch[0], t[o].ch[1]);
    p = merge(o, p);
    rt = merge(q, p);
}

1.5.3 查询操作#

类似于线段树上二分,不多叙述。

int Query(int val) {
    int res = 0, now = rt;
    while (now) {
        if (t[now].t >= val) now = t[now].ch[0];
        else {
            res += t[t[now].ch[0]].siz + 1;
            now = t[now].ch[1];
        }
    }
    return res + 1;
}

1.5.4 排名操作#

类似于线段树上二分,不多叙述。

int Rank(int x) {
    int now = rt; x --;
    while (now) {
        if (t[t[now].ch[0]].siz > x) {
            now = t[now].ch[0];
        } else if (x == t[t[now].ch[0]].siz) {
            return t[now].t;
        } else {
            x -= t[t[now].ch[0]].siz + 1;
            now = t[now].ch[1];
        }
    }
    return -1;
}

1.5.5 前缀查询#

我们把平衡树分裂成 v<v,在 <v 的部分去暴力跑最小值即可。

int Precursor(int val) {
    int p, q;
    split(rt, val, p, q);
    int x = q, res = -1;
    while (x) {
        res = t[x].t;
        if (t[x].ch[1]) x = t[x].ch[1];
        else break;
    }
    rt = merge(q, p);
    return res;
}

1.5.6 后缀查询#

我们把平衡树分裂成 >vv,在 <v 的部分去暴力跑最大值即可。

int Suffix(int val) {
    int p, q;
    split(rt, val + 1, p, q);
    int x = p, res = -1;
    while (x) {
        res = t[x].t;
        if (t[x].ch[0]) x = t[x].ch[0];
        else break;
    }
    rt = merge(q, p);
    return res;
}

1.6 FHQ-Treap 维护区间信息#

1.6.1 FHQ-Treap 维护区间思想#

因为在维护区间的时候,一般把权值的中序遍历视为这个序列当前的顺序,所以 一般情况下,维护区间信息的 FHQ 权值不满足小根堆。

于是在分裂时,我们就只能 按照大小分裂

由于 FHQ-Treap 的分裂比较厉害,我们可以通过两次分裂轻松的把代表 [l,r] 的子树给裂出来。

但是如果直接整个子树更新,时间复杂度肯定爆炸。所以我们要考虑学习线段树,在平衡树上打 tag 即可。

以下是一个区间翻转的例子。

void Reverse(int l, int r) {
    long long x, y, z;
    split(rt, r, x, y);
    split(y, l - 1, z, y);
    t[z].rev ^= 1;
    y = merge(y, z);
    rt = merge(y, x);
}

1.6.2 维护区间信息需要注意的点#

  • 一定要清空 0 号节点。由于在没有儿子的时候儿子变量存储的是 0,导致在 pushUp 的时候有可能会把 0 节点的信息(也就是本来没有的)给 pushUp 到正常节点上

1.7 FHQ-Treap 经典例题#

I 序列终结者#

FHQ-Treap 板子题,就是单纯的区间操作。用来练一下手。

/*******************************
| Author:  DE_aemmprty
| Problem: P4146 序列终结者
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/P4146
| When:    2024-04-03 19:10:26
| 
| Memory:  128 MB
| Time:    1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 5e4 + 7;
mt19937 rnd(time(0));

int n, m;

struct FHQ {
    long long t, a, ch[2], val;
    long long rev, add, siz, mx;
} t[N];

struct FHQ_Treap {
    int cnt, rt;
    void init() { cnt = 0, rt = 0; t[0].mx = -2e18;}
    int newNode(int v) {
        t[++ cnt] = {v, rnd(), {0, 0}, 0, 0, 0, 1, 0};
        return cnt;
    }
    void pushUp(int x) {
        t[x].rev = t[x].add = 0;
        t[x].mx = max(t[x].val, max(t[t[x].ch[0]].mx, t[t[x].ch[1]].mx));
        t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
    }
    void pushDown(int x) {
        if (t[x].rev) {
            t[t[x].ch[0]].rev ^= 1;
            t[t[x].ch[1]].rev ^= 1;
            swap(t[x].ch[0], t[x].ch[1]);
            t[x].rev = 0;
        }
        t[t[x].ch[0]].add += t[x].add, t[t[x].ch[1]].add += t[x].add;
        t[t[x].ch[0]].mx += t[x].add,  t[t[x].ch[1]].mx += t[x].add;
        t[t[x].ch[0]].val += t[x].add, t[t[x].ch[1]].val += t[x].add;
        t[x].add = 0;
    }
    int merge(int x, int y) {
        if (!x || !y) return x + y;
        pushDown(x), pushDown(y);
        if (t[x].a < t[y].a) {
            t[x].ch[1] = merge(t[x].ch[1], y);
            pushUp(x);
            return x;
        }
        else {
            t[y].ch[0] = merge(x, t[y].ch[0]);
            pushUp(y);
            return y;
        }
    }
    void split(int now, int k, long long &x, long long &y) {
        if (!now) return x = y = 0, void();
        pushDown(now);
        if (t[t[now].ch[0]].siz >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
        else                          y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[now].ch[1]);
        pushUp(now);
    }
    void Insert(int v) {
        long long x, y;
        split(rt, v - 1, x, y);
        y = merge(y, newNode(v));
        rt = merge(y, x);
    }
    void Update(int l, int r, long long v) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        t[z].add += v, t[z].val += v, t[z].mx += v;
        y = merge(y, z);
        rt = merge(y, x);
    }
    void Reverse(int l, int r) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        t[z].rev ^= 1;
        y = merge(y, z);
        rt = merge(y, x);
    }
    long long getMax(int l, int r) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        long long res = t[z].mx;
        y = merge(y, z);
        rt = merge(y, x);
        return res;
    }
} F;

void solve() {
    n = read(), m = read();
    F.init();
    for (int i = 1; i <= n; i ++)
        F.Insert(i);
    while (m --) {
        long long op = read(), l, r, v;
        if (op == 1) {
            l = read(), r = read(), v = read();
            F.Update(l, r, v);
        } else if (op == 2) {
            l = read(), r = read();
            F.Reverse(l, r);
        } else {
            l = read(), r = read();
            cout << F.getMax(l, r) << '\n';
        }
    }
}

signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

II Peaks#

我们把询问先离线下来,然后从下往上扫。

使用并查集维护连通块关系,使用非旋 Treap 来进行第 k 大的维护。

#include <bits/stdc++.h>
using namespace std;

const int N = 5e5 + 7;

int n, m, q;

struct Edge {
	int u, v, w;
	bool operator < (const Edge &x) const {
		return w < x.w;
	}
} e[N];

long long ans[N], h[N];

struct Query {
	int v, x, k, id;
	bool operator < (const Query &qwq) const {
		return x < qwq.x;
	}
} p[N];

namespace DSU {
	int fa[N];
	void init() { for (int i = 1; i <= n; i ++) fa[i] = i;}
	int find(int x) { return x == fa[x] ? x : (fa[x] = find(fa[x]));}
	void Union(int i, int j) { fa[find(j)] = find(i);}
} using namespace DSU;

long long read() {
	long long x = 0, k = 1; char c = getchar();
	while ((c < '0' || c > '9') && c != '-') c = getchar();
	if (c == '-') k = -1, c = getchar();
	while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
	return x * k;
}

mt19937 rnd(time(0));

struct FHQ {
	int ch[2];
	long long a;
	int t, siz;	
} t[N];

int cnt;

namespace FHQ_Treap {
	int newNode(int v) { t[++ cnt] = {{0, 0}, (long long) rnd(), v, 1}; return cnt;}
	void pushUp(int x) { t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;}
	int merge(int x, int y) {
		if (!x || !y) return x + y;
		if (t[x].a < t[y].a) {
			t[x].ch[1] = merge(t[x].ch[1], y);
			pushUp(x);
			return x;
		} else {
			t[y].ch[0] = merge(x, t[y].ch[0]);
			pushUp(y);
			return y;
		}
	}
	void split(int now, int k, int &x, int &y) {
		if (!now) return x = y = 0, void();
		if (t[now].t < k) {
			x = now;
			split(t[now].ch[1], k, t[now].ch[1], y);
		} else {
			y = now;
			split(t[now].ch[0], k, x, t[now].ch[0]);
		}
		pushUp(now);
	}
	int insert(int now, int v) {
		int x, y;
		split(now, h[v], x, y);
		x = merge(x, merge(v, y));
		if (x == v) Union(v, now);
		else 		Union(now, v); 
		return x;
	}
	int kth(int now, int k) {
		while (now) {
			if (k <= t[t[now].ch[1]].siz) now = t[now].ch[1];
			else if (k == t[t[now].ch[1]].siz + 1) return t[now].t;
			else k -= t[t[now].ch[1]].siz + 1, now = t[now].ch[0];
		}
		return -1;
	}
} using namespace FHQ_Treap;

void Merge(int p, int q) {
	int rp = find(p), rq = find(q);
	if (rp == rq) return ;
	if (t[rp].siz > t[rq].siz) swap(rp, rq);
	int siz = t[rp].siz;
	while (siz) {
		int lx = t[rp].ch[0], rx = t[rp].ch[1];
		t[rp].ch[0] = t[rp].ch[1] = 0; t[rp].siz = 1;
		rq = insert(rq, rp);
		rp = merge(lx, rx);
		siz --;
	}
}

signed main() {
	n = read(), m = read(), q = read();
	init();
	for (int i = 1; i <= n; i ++) h[i] = read();
	for (int i = 1; i <= m; i ++) 
		e[i].u = read(), e[i].v = read(), e[i].w = read();
	sort(e + 1, e + m + 1);
	for (int i = 1; i <= q; i ++)
		p[i].v = read(), p[i].x = read(), p[i].k = read(), p[i].id = i;
	for (int i = 1; i <= n; i ++) newNode(h[i]);
	sort(p + 1, p + q + 1);
	for (int i = 1, j = 1; i <= q; i ++) {
		while (j <= m && e[j].w <= p[i].x)
			Merge(e[j].u, e[j].v), j ++;
		ans[p[i].id] = kth(find(p[i].v), p[i].k);
	}
	for (int i = 1; i <= q; i ++)
		cout << ans[i] << '\n';
	return 0;
}

III 队列#

IV 火星人 prefix#

对于后缀求 LCP,较简单的求法有后缀数组,后缀自动机和二分加哈希。

容易发现,后缀数组和后缀自动机比较难以进行修改。遂进行二分加哈希。

我们可以维护这一个字符串的前缀哈希值。接下来,考虑如何维护:

  • 对于修改操作,本质上相当于对一个后缀进行 区间加法

  • 对于插入操作,本质上相当于对一个后缀进行 区间加法区间乘法(整体偏移)插入

  • 对于查询操作,本质上相当于查询 p 的哈希值然后进行二分。

总结一下,我们发现这个数据结构需要做到 区间操作插入。非旋 Treap 可以做到这一点。

// 咕。

作者:DE_aemmprty

出处:https://www.cnblogs.com/aemmprty/p/18104830

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   DE_aemmprty  阅读(28)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示