FHQ Treap

FHQ Treap

前言 | Preface

前置知识

二叉搜索树、堆、递归

简介

Treap 的思路是通过赋予节点随机的优先级,然后让优先级满足堆的条件,以约束树高来保障时间复杂度

因为我们发现随机值性质十分的弱,可以轻松的卡过,所以我们就会想到利用随机

但是数据肯定不会是随机,不然暴力都能过

但是我们又可以自己为数据附上随机值,Treap 就是这种思路

由于涉及随机的赋值,复杂度分析会稍难一些

根据具体的实现,有无旋(FHQ,和有旋两种

先学的较为简单且通用的 FHQ Treap

实现

FHQ Treap 主要就是两个操作:split 和 merge

框架

/*
key of left child node <= key of node <= key of right node

priority of left or right >= this priority
*/

struct node {
    // children
    int left, right;
    // value
    int key, priority, size;
    // tag
    int rev;
} Treap[maxn];

int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root

这里我们让这棵树的优先级满足小根堆的性质

接下来用 left 表示裂开的“左”树,right 表示“右”树,如果分裂两次,则用 mid 表示“中”树

maintain

void maintain(int id) {
    return Treap[id].size = Treap[lson(id)].size + Treap[rson(id)].size + 1, void();
}

向上传递 size

pushdown

void pushdown(int u) {
    if (Treap[u].rev) {
        swap(lson(u), rson(u));
        if (lson(u)) Treap[lson(u)].rev ^= 1;
        if (rson(u)) Treap[rson(u)].rev ^= 1;
        Treap[u].rev = 0;
    }
}

这个是后面的翻转操作所需要的

New Node

int newNode(int value) {
    Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0};
    return totalNode;
}

Split

有别于线段树的 split 操作,这里是将所有的键值 key 小于等于 value 的拎出,重新构成了两棵树

void split(int u, int value, int &left, int &right) {
    if (!u) {
        left = right = 0;
        return ;
    }
    pushdown(u);
    if (Treap[u].key <= value) {
        left = u;
        split(rson(u), value, rson(u), right);
    } else {
        right = u;
        split(lson(u), value, left, lson(u));
    }
    maintain(u);
}

这个得手动模拟一番才能很好的理解,left 和 right 表示分裂后的两棵树

它的思路是将 u 的 key 与 value 比较,利用搜索树的性质发现已将确定的子树,遍历另外一棵重复此操作

然后利用一边的空节点回溯,将本来在不应在的位置的节点抹去(赋值为 \(0\)

自己手撕一下会很清晰(这里不用考虑 priority ,因为一直保持着原树的祖先关系

Merge

将优先级小的放在上面

int merge(int left, int right) {
		if (!left || !right) return left | right;
		
		if (Treap[left].priority < Treap[right].priority) {
			pushdown(left);
			rson(left) = merge(rson(left), right);
			maintain(left);
			return left;
		} else {
			pushdown(right);
			lson(right) = merge(left, lson(right));
			maintain(right);
			return right;
		}
	}

Insert

将 \(\le v\) 和 \(\gt v\) 的分开,在中间插入

void insert(int value) {
    int left, right;
    split(root, value, left, right);
    root = merge(merge(left, newNode(value)), right);
}

Erase

分成三棵,一棵 \(\lt v\),一棵 \(= v\),另一棵 \(\gt v\)

将中间的根节点删去(合并它的左右儿子

void erase(int value) {
    int left, mid, right;
    split(root, value, left, right);
    split(left, value - 1, left, mid);
    if (mid) mid = merge(lson(mid), rson(mid));
    root = merge(merge(left, mid), right);
}

如果是将全部值为 \(v\) 的删去,则直接合并 left 和 right

root = merge(left, right);

rank

将 \(\lt v\) 的裂开,Treap[left].size + 1 就是它的排名

int Rank(int value) {
    int left = 0, right = 0, result;
    split(root, value - 1, left, right);
    result = Treap[left].size + 1;
    root = merge(left, right);
    return result;
}

第 \(k\) 大

和线段树的操作差不多

int kth_element(int u, int k) {
    if (k == Treap[lson(u)].size + 1) return u;
    if (k <= Treap[lson(u)].size) return kth_element(lson(u), k);
    else return kth_element(rson(u), k - Treap[lson(u)].size - 1);
}

前驱、后继

将 \(\lt v\) 的裂开,left 中第 Treap[left].size 大的键值就是前驱
同理,将 \(\gt v\) 的裂开,right 中第 \(1\) 大的键值就是后继

/*
precursor
*/
int precursor(int value) {
    int left = 0, right = 0, result;
    split(root, value - 1, left, right);
    result = Treap[kth_element(left, Treap[left].size)].key;
    root = merge(left, right);
    return result;
}

/*
successor
*/
int successor(int value) {
    int left = 0, right = 0, result;
    split(root, value, left, right);
    result = Treap[kth_element(right, 1)].key;
    root = merge(left, right);
    return result;
}

汇总

总的代码就是如此了

View Code
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());

namespace FHQ_Treap{
	
	/*
	key of left child node <= key of node <= key of right node
	priority of left or right <= this priority
	
	*/
	
	struct node {
		// children
		int left, right;
		// value
		int key, priority, size;
		// tag
		int rev;
	} Treap[maxn];
	
	int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root
	
	int newNode(int value) {
		Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0};
		return totalNode;
	}
	
#define lson(u) Treap[u].left
#define rson(u) Treap[u].right
	
	void maintain(int id) {
		return Treap[id].size = Treap[lson(id)].size + Treap[rson(id)].size + 1, void();
	}
	// split (by some value)
	
	/*
	push down the tag "rev"
	*/
	void pushdown(int u) {
		if (Treap[u].rev) {
			swap(lson(u), rson(u));
			if (lson(u)) Treap[lson(u)].rev ^= 1;
			if (rson(u)) Treap[rson(u)].rev ^= 1;
			Treap[u].rev = 0;
		}
	}
	
	/*
	if the key of root is less than the splitting standard,we only need	consider right sub-tree and vice versa
	
	the complexity of splitting is O(height of the tree) = O(log n)
	
	why the expection of the height is O(log n)
	
	the probility of that node x which is d far away from u is an ancetor of u is 1/d
	
	so E[depth(u)] = \sum_{i = 1}^{n} 1/i = O(log n)
	*/
	void split(int u, int value, int &left, int &right) {
		if (!u) {
			left = right = 0;
			return ;
		}
		pushdown(u);
		if (Treap[u].key <= value) {
			left = u;
			split(rson(u), value, rson(u), right);
		} else {
			right = u;
			split(lson(u), value, left, lson(u));
		}
		maintain(u);
	}
	
	/*
	split by rank
	*/
	void splitByRank(int u, int k, int &left, int &right) {
		if (!u) {
			left = right = 0;
			return ;
		}
		pushdown(u);
		if (Treap[lson(u)].size + 1 <= k) {
			left = u;
			splitByRank(rson(u), k - (Treap[lson(u)].size + 1), rson(u), right);
		} else {
			right = u;
			splitByRank(lson(u), k, left, lson(u));
		}
		maintain(u);
	}
	
	/*
	smilar to segment tree merging
	*/
	int merge(int left, int right) {
		if (!left || !right) return left | right;
		
		if (Treap[left].priority < Treap[right].priority) {
			pushdown(left);
			rson(left) = merge(rson(left), right);
			maintain(left);
			return left;
		} else {
			pushdown(right);
			lson(right) = merge(left, lson(right));
			maintain(right);
			return right;
		}
	}
	
	/*
	reverse
	*/
	void reverse(int L, int R) {
		int left = 0, mid = 0, right = 0;
		splitByRank(root, L - 1, left, right);
		splitByRank(right, R - L + 1, mid, right);
		Treap[mid].rev ^= 1;
		root = merge(merge(left, mid), right);
	}
	
	/*
	insert
	*/
	void insert(int value) {
		int left, right;
		split(root, value, left, right);
		root = merge(merge(left, newNode(value)), right);
	}
	
	/*
	delete
	*/
	void erase(int value) {
		int left, mid, right;
		split(root, value, left, right);
		split(left, value - 1, left, mid);
		if (mid) mid = merge(lson(mid), rson(mid));
		root = merge(merge(left, mid), right);
	}
	
	/*
	rank
	*/
	int Rank(int value) {
		int left = 0, right = 0, result;
		split(root, value - 1, left, right);
		result = Treap[left].size + 1;
		root = merge(left, right);
		return result;
	}
	
	/*
	k-th element
	*/
	int kth_element(int u, int k) {
		if (k == Treap[lson(u)].size + 1) return u;
		if (k <= Treap[lson(u)].size) return kth_element(lson(u), k);
		else return kth_element(rson(u), k - Treap[lson(u)].size - 1);
	}
	
	/*
	precursor
	*/
	int precursor(int value) {
		int left = 0, right = 0, result;
		split(root, value - 1, left, right);
		result = Treap[kth_element(left, Treap[left].size)].key;
		root = merge(left, right);
		return result;
	}
	
	/*
	successor
	*/
	int successor(int value) {
		int left = 0, right = 0, result;
		split(root, value, left, right);
		result = Treap[kth_element(right, 1)].key;
		root = merge(left, right);
		return result;
	}
	
	/*
	???
	*/
	void inorder(int u) {
		if (!u) return ;
		pushdown(u);
		inorder(lson(u));
		cout << u << " ";
		inorder(rson(u));
	}
	
#undef lson
#undef rson
}

例题

P3369 【模板】普通平衡树

纯模版

cin >> n;
	
for (int i = 1; i <= n; i++) {
	int opt, x;
	cin >> opt >> x;
	if (opt == 1) FHQ_Treap::insert(x);
	else if (opt == 2) FHQ_Treap::erase(x);
	else if (opt == 3) cout << FHQ_Treap::Rank(x) << "\n";
	else if (opt == 4) cout << FHQ_Treap::Treap[FHQ_Treap::kth_element(FHQ_Treap::root, x)].key << "\n";
	else if (opt == 5) cout << FHQ_Treap::precursor(x) << "\n";
	else cout << FHQ_Treap::successor(x) << "\n";
}

P3391 【模板】文艺平衡树

还是模版(直接用 reverse

void inorder(int u) {
	if (!u) return ;
	pushdown(u);
	inorder(lson(u));
	cout << u << " ";
	inorder(rson(u));
}

cin >> n >> m;
	
for (int i = 1; i <= n; i++) insert(i);

for (int i = 1; i <= m; i++) {
	int left, right;
	cin >> left >> right;
	FHQ_Treap::reverse(left, right);
}

inorder(root);

P1486 [NOI2004] 郁闷的出纳员

这个就要动一下脑子了

我们拿一个 delta 记录全局的工资,然后 实际的工资 = Treap 中的工资 + delta

不过新入职的员工的工资要减去 delta(好悲催

cin >> n >> m;

int cnt = 0;

for (int i = 1; i <= n; i++) {
	char type;
	cin >> type;
	int k;
	cin >> k;
	
	if (type == 'I') {
		if (k >= m) {
			k -= delta;
			insert(k);
		}
	} else if (type == 'A') {
		delta += k;
	} else if (type == 'S') {
		delta -= k;
		int L = 0, R = 0;
		split(root, m - delta - 1, L, R);
		root = R;
		cnt += Treap[L].size;
	} else {
		if (k > Treap[root].size) cout << -1 << "\n";
		else cout << Treap[kth_element(root, Treap[root].size - k + 1)].key + delta << "\n";
	}
}

cout << cnt << "\n";

[SPOJ227] ORDERS - Ordering the Soldiers

这个就要大动一下脑子了

正着走会被后面的兵影响到,不妨倒着走

cin >> n;
root = 0;

for (int i = 1; i <= n; i++) cin >> arr[i];

for (int i = 1; i <= n; i++)
	root = merge(root, newNode(i)); // 初始化

for (int i = n; i >= 1; i--) {
	int pos = i - arr[i];
	int left, mid, right;
	splitByRank(root, pos - 1, left, right); // 分裂成排名 [1, pos - 1] 和 [pos, n] 的
	splitByRank(right, 1, mid, right); // 将第一个弄出来,这个就是我们要的
	answer[i] = Treap[mid].key;
	root = merge(left, right); // 合并回去
}

for (int i = 1; i <= n; i++) cout << answer[i] << " \n"[i == n];

CF702F T-Shirts

这个更是要动一一一下脑子

既然人去匹配 T shirts 很复杂,我们不妨反过来,让 T shirts 去匹配人

将 T shirts 按照品质从高到低排序

假设现在的 T shirts 的品质是 p,价格是 c

我们让所有买得起的都买下这一件

现在先粗略的分成两个部分:\([1,c]\),\((c,\infin)\)

然后我们发现,在后面一个部分中,\((c,2c]\) 的部分在减去 \(c\) 后会变成 \([1,c]\) 的部分

所以我们分成三个部分:\([1,c]\),\((c,2c]\),\((2c, \infin)\)

第一个不用管,第二个暴力减了合并到第一个上面,第三个直接减

现在考虑暴力加到第一个部分上的复杂度

假设 \(x \in (c,2c]\),考虑更新后的比例 \(\frac{x - c}{x}\),在 \(x = 2c\) 的时候取得最大,为 \(\frac12\)

所以,\(x\) 至少都减小了 \(\frac12\),所以最多 \(\mathcal O(\log n)\) 次修改

所以复杂度为 \(\mathcal O(n\log n\log w)\),其中 \(w\) 是值域大小

struct node {
	// children
	int left, right;
	// value
	int key, priority, size;
	// tag
	int rev;
	
	// problem
	int tag1, tag2;
	
	int number;
	
	int id;
} Treap[maxn];

int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root

int newNode(int value, int id) {
	Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0, 0, 0, 0, id};
	return totalNode;
}

// ----------------

void pushdown(int u) {
	if (Treap[u].rev) {
		swap(lson(u), rson(u));
		if (lson(u)) Treap[lson(u)].rev ^= 1;
		if (rson(u)) Treap[rson(u)].rev ^= 1;
	}
	if (Treap[u].tag1) {
		int t1 = Treap[u].tag1;
		if (lson(u)) Treap[lson(u)].tag1 += t1, Treap[lson(u)].key += t1;
		if (rson(u)) Treap[rson(u)].tag1 += t1, Treap[rson(u)].key += t1;
	}
	if (Treap[u].tag2) {
		int t2 = Treap[u].tag2;
		if (lson(u)) Treap[lson(u)].tag2 += t2, Treap[lson(u)].number += t2;
		if (rson(u)) Treap[rson(u)].tag2 += t2, Treap[rson(u)].number += t2;
	}
	Treap[u].rev = Treap[u].tag1 = Treap[u].tag2 = 0;
}

// -------------

void add(int u, int &rt) {
	if (!u) return ;
	pushdown(u);
	add(lson(u), rt);
	add(rson(u), rt);
	lson(u) = rson(u) = 0;
	insert(u, rt);
}

void getAnswer(int u) {
	if (!u) return ;
	pushdown(u);
	getAnswer(lson(u));
	getAnswer(rson(u));
	answer[Treap[u].id] = Treap[u].number;
}

// ------------

struct Node {
	int q, c;
	
} shirts[maxn];

bool cmp(Node a, Node b) {
	if (a.q != b.q) return a.q > b.q;
	else return a.c < b.c;
}

void solve() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> shirts[i].c >> shirts[i].q;
	
	sort(shirts + 1, shirts + n + 1, cmp);
	
	cin >> m;
	
	for (int i = 1; i <= m; i++) {
		int x;
		cin >> x;
		int u = newNode(x, i);
		insert(u, root);
	}
	
	for (int i = 1; i <= n; i++) {
		int c = shirts[i].c, q = shirts[i].q;
		
		int left = 0, mid = 0, right = 0;
		
		split(root, c - 1, left, mid);
		split(mid, 2 * c - 1, mid, right);
		
		if (right) {
			Treap[right].key -= c;
			Treap[right].number++;
			Treap[right].tag1 -= c;
			Treap[right].tag2++;
		}
		
		if (mid) {
			Treap[mid].key -= c;
			Treap[mid].number++;
			Treap[mid].tag1 -= c;
			Treap[mid].tag2++;
			add(mid, left);
		}
		
		root = FHQ_Treap::merge(left, right);
	}
	
	getAnswer(root);
	
	for (int i = 1; i <= m; i++) cout << answer[i] << " \n"[i == m];
}

P5338 [TJOI2019] 甲苯先生的滚榜

这里假设 \(k\) 是通过数,\(\text{punish}\) 是罚时,常数 \(\text{base} = 1.5 \times 10 ^ 6\)

考虑到 \(\sum \text{punish}_i \le 1.5 \times 10^6\),所以我们可以构建一个 1.5e6 进制数

由于 \(\text{punish}\) 相较于 \(k\) 来言对于排名的贡献是负的,所以取它的相反数

而 \(k \le 10^6\),所以整个数 \(\le 10^{13}\),要用 long long(这个沙子没开 long long 交了五六次

其他的,由于我不想重构代码,写一个第 \(k\) 小,所以去了一个相反数

struct it{
	ll tot, punish;
} manba_out[maxn];

// ----
	
/*
insert
*/
void insert(ll value, int punish) {
	int left = 0, right = 0;
	
	ll key = -(value * base - punish);
	
	split(root, key, left, right);
	root = merge(merge(left, newNode(value, punish)), right);
}

/*
delete
*/
void erase(ll value, int punish) {
	int left, mid, right;
	ll val = -(1ll * value * base - punish);
	split(root, val, left, right);
	split(left, val - 1, left, mid);
	if (mid) mid = merge(lson(mid), rson(mid));
	root = merge(merge(left, mid), right);
}

/*
1
*/
void update(ui Ria, ui Rib) {
	erase(manba_out[Ria].tot, manba_out[Ria].punish);
	manba_out[Ria].tot++;
	manba_out[Ria].punish += Rib;
	insert(manba_out[Ria].tot, manba_out[Ria].punish);
}

// --------

/*
get answer
*/

int getRank(ll value, int punish) {
	ll key = -(1ll * value * base - punish);
	int left = 0, right = 0;
	split(root, key - 1, left, right);
	int result = Treap[left].size;
	root = merge(left, right);
	return result;
}

// -------

/*
solve
*/

ui seed;
int m;
ui randNum( ui& seed , ui last , const ui m){ 
	seed = seed * 17 + last ; return seed % m + 1; 
}

ui last = 7;

void solve() {
	Treap[1] = node{0, 0, 0, 0, 0, 0};
	totalNode = 0;
	root = 0;

	cin >> m >> n >> seed;
	
	for (int i = 1; i <= m; i++) manba_out[i].tot = manba_out[i].punish = 0;
	
	ui mm = m;
	for (int i = 1; i <= n; i++) {
		ui Ria = randNum(seed, last, mm), Rib = randNum(seed, last, mm);
		update(Ria, Rib);
		last = getRank(manba_out[Ria].tot, manba_out[Ria].punish);
		cout << last << "\n";
	}
//	
//	inorder(root);
}

注意 insert 里面一定要用 key = -(value * base - punish),不要直接按照 value 分裂(这是有血泪教训的

复杂度分析

简要分析

考虑节点 x,y,假设 y 是 x 的祖先,则在 \([x, y]\) 中,y 的优先级是最小的

而优先级又是均匀取值的(这里这么假设

则“距离”(优先级)节点 x 的 节点 y ,\(\text{Pr}\{y = \operatorname{ancetor}(x)\} = \frac{1}{d}\)

则期望为:

\[\mathbb{E}\{\operatorname{depth} \} = H_n = \sum_{d = 1}^{n} \frac{1}{d} = \ln n + \gamma + \epsilon_n = \mathcal O(\log n) \]

严格分析

考虑分析树高的期望(算法复杂度由树高保证

\[H(m) = 1 + \frac{1}{m}\sum _{i = 1}^{m} \max H(i - 1), H(m - i) \]

但是如果采用均值:\(\mathbb E[h] \le \mathbb E[\text{left}] + \mathbb E[\text{right}]\),过于松了

考虑随机变量 \(\mathbb I_{i,j}\) 表示 \(n_i\) 是否是 \(n_j\) 的祖先

我们发现,若 \(n_i\) 是 \(n_j\) 的祖先,那么 在区间 \([\min(i,j),\max(i,j)]\) 中,\(n_i\) 的优先级最高

由于优先级均匀分布,\(\operatorname{Pr}\{\mathbb I_{i,j} = 1\} = \frac{1}{|j - i| + 1}\)

所以:

\[\mathbb E[\operatorname{depth}(j)] = \sum_{i \not = j} \frac{1}{|j-i|+1} \le 2 H_n = 2(\ln n + \gamma + \epsilon_n) = \mathcal O(\log n) \]

其实到这里我们就可将树高粗略的估计了:

\[\text{height} = \max \text {depth}_i = \mathcal O(\log n) \]

现在,我们考虑更进一步,我们想要证明:

\[\exist c,\lambda \gt 0 \text{ s.t. } \operatorname{Pr} \{ \text{depth} \ge c \log n \} \le n^{-\lambda} \]

这里考虑从组合计数的角度证明

由于具体的分布并不会怎么影响最后的结果,所以这里假设选取某个数作为优先级的概率是均匀分布的

具体的,我们构建出来的树仅仅是由大小关系确定的

考虑具体的情况,我们容易知道 \(\text{depth} \ge t\) 等价于存在一个大小为 \(t\) 的祖先集合

而我们容易知道(其实在前面:

我们发现,若 \(n_i\) 是 \(n_j\) 的祖先,那么 在区间 \([\min(i,j),\max(i,j)]\) 中,\(n_i\) 的优先级最高

所以一个祖先链肯定满足一个严格的全序关系(单调递增,且每个数在它们的区间内是最大的

我们一共有 \(\binom{n}{t}\) 种选取方案,由于取值是均匀地,所以在所有的排列中,仅有一种是满足要求的

所以选到正确的选择的概率是 \(\frac{1}{t!}\)

所以有:

\[\operatorname{Pr}\{\text{depth} \ge t \} = \frac{1}{t!}\binom{n}{t} \]

考虑近似

利用上界(可由 Stirling 公式推出):

\[\binom{n}{t} \le \frac{n^t}{t!}, \, \text{so} \, \binom{n}{t} \frac{1}{t!} \le \frac{n^t}{(t!)^2}. \]

更进一步,Stirling 近似 \(t! \sim \sqrt{2\pi t}(t/e)^t\) 给出:

\[\frac{n^t}{(t!)^2} \le \left( \frac{e^2 n}{t^2} \right)^t \]

更紧的常用上界是:

\[\binom{n}{t} \frac{1}{t!} \le \left( \frac{e n}{t^2} \right)^t \]

所以这里我们使 \(t = c \log n\),并使 \(c\) 足够大,使得 \(\exist \epsilon \gt 0, \, \text{ s.t. } \left(\frac{en}{c^2\log^2n} \right) \le n^{-\epsilon}\)

所以就有:

\[\operatorname{Pr}\{\text{depth} \ge c \log n \} \le \left( \frac{en}{t^2} \right)^t = \left(\frac{en}{c^2\log^2n} \right)^{c\log n} \le (n^{-\epsilon})^{c \log n} = n ^{-2\epsilon C} = n^{-\lambda} \]

教材上的断言是 \(\operatorname{Pr}\{\text{depth} \ge c \log n \} \le n^{-2}\)

有了这个,我们就可以发现,树高是以高概率保证 \(\mathcal O(\log n)\) 的

posted @ 2026-07-10 21:33  Yangyihao  阅读(13)  评论(0)    收藏  举报