• 左偏树


    博主学习左偏树时阅读的博客

    前置芝士

    了解堆。

    引言

    如果要求你合并两个优先队列,并且合并后的结果仍然符合优先队列的性质,此时怎么做?
    显然,此处不能使用普通的堆。那如何完成操作呢?这时候就需要使用一种特殊的堆了:左偏树。


    正文

    左偏树的名,乍一听,像是棵向左偏的树。实际上它的确是一棵向左偏的树(雾),但是向左偏的并不是节点。具体是什么“左偏”会在下文中讲解。

    概念定义

    外节点

    当且仅当此节点的左子节点和右子节点中的一个是空节点,或者左子节点和右子节点都是空节点,该节点才能被称作外节点。

    如图:A节点不属于外节点,因为它有左右子节点;B、C节点属于外节点,因为B节点没有右子节点,C节点没有左子节点;D节点也属于外节点,因为它没有左右子节点。

    距离

    左偏树中,点的距离为该点到它的子树中,离它最近的外节点的距离。

    该图中,离根节点最近的外节点为 (C)(A → B → C) 需经过两个节点,所以根节点 (A) 的距离为2。


    性质与推论

    一颗左偏树,必须有如下两个性质:

    · 性质一. 任意节点的权值都要小于其父节点(如果有的话)的权值;

    · 性质二. 任意节点的左子节点的距离一定大于右子节点的距离;

    根据性质,可以得到如下推论:

    · 推论一. 任意一个非外节点的距离为其右子节点的距离 + 1;

    · 推论二. (n) 个点的左偏树的最大距离为 (log(n + 1) - 1。)

    推论一给出了更新节点距离的方法,而推论二保证了左偏树的时间复杂度。


    操作

    合并(merge)

    快速合并两个普通堆基本是天方夜谭,但合并操作在左偏树中的重要性不亚于splay操作在Splay中的重要性,所以请读者务必理解这一模块。
    假设现在给定两棵左偏树,(a, b) 是它们的根节点,现在要将 (b) 合并到 (a) 上。
    根据堆最基础的性质(根节点的权值应小于子节点的权值),(b) 的权值应小于当前遍历到的点(以下称其为 (x) )。故如果遇到 (val_x < val_b) 的情况,则要将 (x)(b) 调换。

    假设 (val_x > val_b) ,则此时应将 (x)(b) 调换。

    调换完成后,继续向右子节点递归,直到当前节点没有右子节点、或者 (b) 被清空。回溯的过程中,更新左右子节点的距离并判断是否满足性质二,若不满足则需调换左右子节点,直到回溯到根节点。

    inline int merge (int x, int y) { //合并两棵树 
    	if (!x) //若该节点无左儿子,则返回右子树;下同 
    		return y;
    	if (!y)
    		return x;
    	if (val[x] > val[y] || (val[x] == val[y] && x > y)) //若左右节点的val值出现矛盾,则进行置换操作 
    		swap (x, y);
    	sons[x][1] = merge (sons[x][1], y); //从右子节点开始进一步进行合并操作 
    	if (dis[sons[x][1]] > dis[sons[x][0]]) //若x,y节点的距离值矛盾,则置换 
    		swap (sons[x][0], sons[x][1]);
    	fa[sons[x][0]] = fa[sons[x][1]] = fa[x] = x; //路径压缩 
    	dis[x] = dis[sons[x][1]] + 1; //更新距离值 
    	return x;
    }
    

    加入单个节点(push/add)

    将单个节点当成只有一个节点的左偏树进行合并。

    inline int add (int value, int x) { //增加一个节点,以子树形式加入 
    	cnt ++;
    	sons[cnt][0] = sons[cnt][1] = dis[cnt] = 0;
    	val[cnt] = value;
    	return merge (cnt, x); //将单独的节点以子树形式合并入整棵树 
    }
    

    弹出根节点(pop/del)

    清空根的信息,将根的左右子节点的父亲值改为自身,再进行合并。

    inline void del (int x) { //删除根节点 
    	int l = sons[x][0], r = sons[x][1];
    	val[x] = -1;
    	fa[l] = l;
    	fa[r] = r;
    	fa[x] = merge (l, r); //合并左右子节点 
    	return ;
    }
    

    建立左偏树(build)

    将所有节点压入一个队列,每次取出队列头的两个节点,进行合并后,将新根节点压入队列最末端,直到队列只剩一个元素(总根)。

    inline int build () { //建造左偏树 
    	queue <int> q;
    	for (int i = 1; i <= n; i ++) //将节点压入序列中 
    		q.push (i);
    	while (!q.empty ())
    		if (q.size () == 1)
    			break ;
    		else { //每次将队首的两个节点当作两颗子树进行合并,将得到的新子树再压入队列 
    			int x = q.front (); q.pop ();
    			int y = q.front (); q.pop ();
    			q.push (merge (x, y));
    		}
    	int rt = q.front (); q.pop (); //更新根节点 
    	return rt;
    }
    

    总代码

    可以在 Luogu3377 上提交。

    //例题并没有用到所有的函数,建议读者打完板子后,自行测试所有函数的正确性。 
    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1e5 + 5, INF = INT_MAX;
    int n, m, cnt, a[N], fa[N], dis[N], val[N], sons[N][2];
    //cnt:当前节点数; fa[x]:x的父节点; dis[x]:x的距离; val[x]:x节点的值; sons[x][y]:x节点的y儿子 
    inline int find (int x) { //寻找祖父节点 
    	if (fa[x] != x)
    		return fa[x] = find (fa[x]); //注意此处要路径压缩,否则会在最后一个点超时 
    	return x;
    }
    inline int merge (int x, int y) { //合并两棵树 
    	if (!x) //若该节点无左儿子,则返回右子树;下同 
    		return y;
    	if (!y)
    		return x;
    	if (val[x] > val[y] || (val[x] == val[y] && x > y)) //若左右节点的val值出现矛盾,则进行置换操作 
    		swap (x, y);
    	sons[x][1] = merge (sons[x][1], y); //从右子节点开始进一步进行合并操作 
    	if (dis[sons[x][1]] > dis[sons[x][0]]) //若x,y节点的距离值矛盾,则置换 
    		swap (sons[x][0], sons[x][1]);
    	fa[sons[x][0]] = fa[sons[x][1]] = fa[x] = x; //路径压缩 
    	dis[x] = dis[sons[x][1]] + 1; //更新距离值 
    	return x;
    }
    inline int add (int value, int x) { //增加一个节点,以子树形式加入 
    	cnt ++;
    	sons[cnt][0] = sons[cnt][1] = dis[cnt] = 0;
    	val[cnt] = value;
    	return merge (cnt, x); //将单独的节点以子树形式合并入整棵树 
    }
    inline void del (int x) { //删除根节点 
    	int l = sons[x][0], r = sons[x][1];
    	val[x] = -1;
    	fa[l] = l;
    	fa[r] = r;
    	fa[x] = merge (l, r); //合并左右子节点 
    	return ;
    }
    inline int build () { //建造左偏树 
    	queue <int> q;
    	for (int i = 1; i <= n; i ++) //将节点压入序列中 
    		q.push (i);
    	while (!q.empty ())
    		if (q.size () == 1)
    			break ;
    		else { //每次将队首的两个节点当作两颗子树进行合并,将得到的新子树再压入队列 
    			int x = q.front (); q.pop ();
    			int y = q.front (); q.pop ();
    			q.push (merge (x, y));
    		}
    	int rt = q.front (); q.pop (); //更新根节点 
    	return rt;
    }
    int opt, firs, seco;
    int main () {
    	scanf ("%d%d", &n, &m);
    	dis[0] = -1;
    	for (int i = 1; i <= n; i ++) {
    		scanf ("%d", &val[i]);
    		fa[i] = i;
    	}
    	for (int i = 1; i <= m; i ++) {
    		scanf ("%d", &opt);
    		if (opt == 1) {
    			scanf ("%d%d", &firs, &seco);
    			int xx = find (firs), yy = find (seco);
    			if (val[firs] == -1 || val[seco] == -1 || xx == yy)
    				continue ;
    			else
    				fa[xx] = fa[yy] = merge (xx, yy);
    		} else {
    			scanf ("%d", &firs);
    			if (val[firs] == -1)
    				puts ("-1");
    			else {
    				int rt = find (firs);
    				printf ("%d
    ", val[rt]);
    				del (rt);
    			}
    		}
    	}
    	return 0;
    }
    
  • 相关阅读:
    Navicat在MySQL中添加外键详细过程
    java绘图原理------在窗口界面(或面板上)画出一张或多张图片问题解决方法
    记录springboot jar包冲突异常处理
    终止线程
    SpringDataJPA
    mysql安装后无法启动问题
    地理空间几种数据格式
    图幅与经纬度之间的换算
    postgresql12集成postgis与timescale
    C# 微信支付 V2
  • 原文地址:https://www.cnblogs.com/HarryHuang2004/p/13264822.html
Copyright © 2020-2023  润新知