浅尝无旋Treap (基于洛谷P3391 文艺平衡树)

21 篇文章 0 订阅

2024/6/28UPD     CSDN又擅自给我改VIP文章:(

说是浅尝吧,确实也挺浅的,完全是基于下面这道题写的↓

洛谷P3391

自己去看题,我是懒得粘了。。。

分析

其实也没有什么好分析的,这就是一道Splay树的模板题,解决一般的Treap不能解决的区间维护问题。

而由于Splay实在是代码又臭又长还难背,于是我们请出了据说时间更加优秀而且迁移性更好的无旋Treap。

无旋Treap

分割 Split

无旋Treap是一种以分裂为基础操作的平衡树类型,本质上还是基于Treap的随机数二叉堆保证平衡,但是其操作却比一般的Treap更加简单,而且功能也更加优秀,能解决一些一般Treap不能解决的问题。

首先来谈一谈无旋Treap最基础的分裂操作。

我们来看到这样的一棵简单的BST:
483315735d524ec6ab6c71bc0e3a32ee.png#pic_center
我们给定一个权值val,这里我们假定为4,我们来把这棵树来截成两段,一边全部小于等于4,一边全部大于4。

于是我们从根开始扫描804d8bbdb5814515968048fd13e8365a.png#pic_center
此时我们发现该节点的权值大于我们所给定的权值4,于是我们可以大胆地把它与其左子树分割开来,并且给他连接到我们假定的R节点上(也就是我们分割后的右半部分,程序的实现是直接把R的根设为权值为5的那个节点),因为这半部分显然是都大于4的。(图上画出的红边实际上是不存在的,其连接的那个点就是R树的根)
58c5b8f0b48449709f990c49941a08eb.png#pic_center

这时我们进入它的左子节点查询,发现其小于权值4,于是我们又大胆的割裂它的右节点与它的联系,然后将其“连接”到L上,成为L树的根节点。80db20126d05421db1607e8573154877.png#pic_center
此时我们我们进入它的右节点进行查询,也就是那个孤零零的三,我们发现它仍然小于权值4,于是我们把它连接到节点2的右儿子上。
5d0c7f9d05af4ddfaeb29efbbd0eaab6.png#pic_center
这里我们没有遇到我们在查询左子树时遇到大于权值4的情况,如果遇到,我们只需要把它安排在5节点的左子树上即可。

下面我们给出了一个更加复杂一些的树:
5832fcc74d674b4d960c3863c52bb415.png#pic_center
下面给出这部分的代码:

	void Update(int rt){
		tr[rt].siz = tr[tr[rt].ls].siz + tr[tr[rt].rs].siz + 1;
	}//这就是非常普通的Treap的Update,这里不做赘述
	void SplitByVal(int rt, int v, int &l, int &r){
		if(!rt){
			l = r = 0;
			return ;
		}
		if(v >= tr[rt].val){
			l = rt;
			SplitByVal(tr[rt].rs, v, tr[rt].rs, r);
		}else{
			r = rt;
			SplitByVal(tr[rt].ls, v, l, tr[rt].ls);
		}
		Update(rt);
	}

合并 Merge

无旋Treap的合并操作则是按照rand值,以小根堆(你要想写大根堆我也不拦着你)的规则将左右两棵树合并成一个。具体操作则是在我们已经给定的两棵树中找到根节点rand值更小的那一个作为根节点。如果我们这里是以左树的根为合并后的树的根,那么我们则将其右子树再与右树递归进行合并操作。如果此时我们选定的是右树则所有操作相反。
70333db4a6eb4191addcf2815f3102e9.png#pic_center
2396bbf758f641cb86fedd0277edcc89.png#pic_center
(上面这张图的左边的节点5掉了,自己脑补一下吧)
e6300f978dbb4bc9ab2b6c157bbd181c.png#pic_center
虽然上图看起来这棵树并不是那么平衡,但是只要我们的数据足够随机,它仍能保证平衡。(由于这张图是我在打散成两个之后才加上的rand值,所以最终构造的和原本的不太一样,理论上重新合成的图应该是和原来一样的?)

关键代码如下:

	int Merge(int l, int r){
		if(!l || !r) return l | r;
		int rt;
		if(tr[l].dat <= tr[r].dat){
			rt = l;
			tr[rt].rs = Merge(tr[l].rs, r);
		}else{
			rt = r;
			tr[rt].ls = Merge(l, tr[r].ls);
		}
		Update(rt);
		return rt;
	}

看到这里,聪明的你一定要问了:“哎呀哎呀,你这只不过是把它拆开了再合并,这有什么用啊!?!?!?”

欸,别着急,马上玄妙的地方就到了。

插入 Insert

假如我们要再上面的树中添加一个权值为7的点,那么我们只需要以7为权值,将整棵树裂开,然后再左树与该节点合并,然后再与右树合并即可。
0362ec66b31e4234926ae40fb1582a8e.png#pic_center
你要想反过来我也不拦着你

关键代码如下:

	int NewPoint(int v){
		++id;
		tr[id].ls = tr[id].rs = 0;
		tr[id].val = v;
		tr[id].dat = rd();
		tr[id].siz = 1;
		return id;
	}//为节点新开一个空间
	void Insert(int &rt, int v){
		int x = 0, y = 0;
		SplitByVal(rt, v, x, y);
		x = Merge(x, NewPoint(v));
		rt = Merge(x, y);
	}

是不是觉得很妙,那我们再来看看删除。

删除 Erase

相同的,我们要删除树上权值为v的节点,我们只是需要按照权值v将树分裂成左右两个(记为x和z),再将左树(x)按照v-1为权值分开(记为x(覆盖掉原本的x)和y),此时我们只需要将x和z合并,那么我们就删除了这棵树上所有权值为v的节点。
7e988842d14b4b0aa65484b55b368efe.png#pic_center
这张图似乎并不是那么严谨,将就着看吧,能懂我意思就行。

关键代码如下:

	void Erase(int &rt, int v){
		int x = 0, y = 0, z = 0;
		SplitByVal(rt, v, x, z);
		SplitByVal(x, v - 1, x, y);
		rt = Merge(x, z); 
	}

但如果我们只需要删除其中一个,那么我们将y的左右子树合并并覆盖掉y,最后让xyz合并即可。

e46d0e7e05fd42c2bcd3005e032aa562.png#pic_center
上图仍是十分不严谨,将就看看。

关键代码如下:

	void Erase(int &rt, int v){
		int x = 0, y = 0, z = 0;
		SplitBySize(rt, v, x, z);
		SplitBySize(x, v - 1, x, y);
		y = Merge(tr[y].ls, tr[y].rs);
		rt = Merge(Merge(x, y), z); 
	}

以上几个就是最基础的四个操作,其他扩展操作我没打,我把我们亲爱的ljx学长的码粘在这里,有兴趣的可以看看:

其他操作

ljx学长的码风真的跟我差别挺大的(我的真就挺丑陋的),这三个操作我还没具体去看,你们先将就着看看吧(主要是变量命名上差别蛮大的)。

    int kth(int o,int k)
    {
        while(1)
        {
            int lsiz = tr[tr[o].ls].siz + 1;
            if(lsiz > k) o = tr[o].ls;
            if(lsiz ==k) break;
            if(lsiz < k) o = tr[o].rs, k -= lsiz;
        }
        return tr[o].val;
    }
    int lower_bound(int &o,int v)
    {
        int x = 0, y = 0, tmp;
        split(o, v - 1, x, y);
        tmp = tr[x].siz + 1;
        o = merge(x, y);
        return tmp;
    }
    int upper_bound(int &o,int v)
    {
        int x = 0, y = 0, tmp;
        split(o, v, x, y);
        tmp = tr[x].siz + 1;
        o = merge(x, y);
        return tmp;
    }

回归正题

现在我们回到这道题上来,这道题只要求了我们将给定的序列进行翻转,输出最终序列,于是乎我们的Erase操作压根就用不上,其实理论上Insert也不用直接Merge就行,但为了就着板子我还是打了。

但是这道题我们不在需要节点上储存的权值了,那么我们怎么知道我们需要的那一位去哪里找呢。显然,当前节点的左子树大小就是它在原序列中的位置,你自行模拟一下这个过程你就明白了。

于是我们将Split操作中的以权值(Val)为基准改成以子树大小(Siz)为基准即可,可以类比于普通Treap里的GetValByRank操作。

关键代码如下:

	void SplitBySize(int rt, int rk, int &l, int &r){
		if(!rt){
			l = r = 0;
			return ;
		}
		if(rk >= tr[tr[rt].ls].siz + 1){
			l = rt;
			SplitBySize(tr[rt].rs, rk - (tr[tr[rt].ls].siz + 1), tr[rt].rs, r);
		}else{
			r = rt;
			SplitBySize(tr[rt].ls, rk, l, tr[rt].ls);
		}
		Update(rt);
	}

那么我们翻转操作怎么办呢?我们可以类比于线段树中的懒标记,给我们所需区间对应的根节点上打上一个tag,意思是这个子树被翻转了,但我还没有调整内部顺序。直到后续操作如Insert、Merge等需要访问其子树时我们再对他进行下放标记。此时我们只需要交换它的左右子树,并给他的左右子树打上tag(或者说调整tag显得更加严谨)。

关键代码如下:

	void PushDown(int rt){
		if(tr[rt].tag){
			swap(tr[rt].ls, tr[rt].rs);
			if(tr[rt].ls) tr[tr[rt].ls].tag ^= 1;
			if(tr[rt].rs) tr[tr[rt].rs].tag ^= 1;
			tr[rt].tag = 0;
		}
	}
	void Reverse(int l, int r){
		int x, y, z;
		SplitBySize(rt, l - 1, x, y);
		SplitBySize(y, r - l + 1, y, z);
		tr[y].tag ^= 1;
		rt = Merge(Merge(x, y), z); 
	}

这样一来我们这道题就轻松解决了。

最后放出完整(并不)的代码:

//头文件和快读省略
struct Point{
	int ls, rs;
	int siz;
	int val, dat;
	int tag;
};
mt19937 rd(60505);
int rt;
struct SplitTreap{//膜拜FHQ大佬orzzzzzzzzzzzzzzzzzz(千足虫(大雾)) 
	Point tr[MAXN];
	int id;
	void Update(int rt){
		tr[rt].siz = tr[tr[rt].ls].siz + tr[tr[rt].rs].siz + 1;
	}
	void PushDown(int rt){
		if(tr[rt].tag){
			swap(tr[rt].ls, tr[rt].rs);
			if(tr[rt].ls) tr[tr[rt].ls].tag ^= 1;
			if(tr[rt].rs) tr[tr[rt].rs].tag ^= 1;
			tr[rt].tag = 0;
		}
	}
	void SplitBySize(int rt, int rk, int &l, int &r){
		if(!rt){
			l = r = 0;
			return ;
		}
		PushDown(rt);
		if(rk >= tr[tr[rt].ls].siz + 1){
			l = rt;
			SplitBySize(tr[rt].rs, rk - (tr[tr[rt].ls].siz + 1), tr[rt].rs, r);
		}else{
			r = rt;
			SplitBySize(tr[rt].ls, rk, l, tr[rt].ls);
		}
		Update(rt);
	}
	int Merge(int l, int r){
		if(!l || !r) return l | r;
		int rt;
		if(tr[l].dat <= tr[r].dat){
			PushDown(l);
			rt = l;
			tr[rt].rs = Merge(tr[l].rs, r);
		}else{
			PushDown(r);
			rt = r;
			tr[rt].ls = Merge(l, tr[r].ls);
		}
		Update(rt);
		return rt;
	}
	int NewPoint(int v){
		++id;
		tr[id].ls = tr[id].rs = 0;
		tr[id].val = v;
		tr[id].dat = rd();
		tr[id].siz = 1;
		return id;
	}
	void Insert(int &rt, int v){
		int x = 0, y = 0;
		SplitBySize(rt, v, x, y);
		x = Merge(x, NewPoint(v));
		rt = Merge(x, y);
	}
	void Erase(int &rt, int v){
		int x = 0, y = 0, z = 0;
		SplitBySize(rt, v, x, z);
		SplitBySize(x, v - 1, x, y);
		y = Merge(tr[y].ls, tr[y].rs);//只删掉一个
		rt = Merge(Merge(x, y), z); 
	}
	void Reverse(int l, int r){
		int x, y, z;
		SplitBySize(rt, l - 1, x, y);
		SplitBySize(y, r - l + 1, y, z);
		tr[y].tag ^= 1;
		rt = Merge(Merge(x, y), z); 
	}
	void Print(int rt){
		if(!rt) return ;
		PushDown(rt);
		Print(tr[rt].ls);
		printf("%d ", tr[rt].val);
		Print(tr[rt].rs);
	}
}STP;

int n, m;

int main()
{
	n = inpt(), m = inpt();
	rt = 0;
	STP.id = 0;
	for(int i = 1; i <= n; i++){
		STP.Insert(rt, i);
	}
	for(int i = 1; i <= m; i++){
		int l = inpt(), r = inpt();
		STP.Reverse(l, r); 
	}
	STP.Print(rt);
	return 0;
}

本来说少写一点点就去写作业的,但是洋洋洒洒写了三个小时,其实也没写出什么东西,今天作业看来是写不了了,这周作业暴多,我可该怎么办啊。

附上我们亲爱的化学老师知道了我们的物理作业时在旁边画上了一个哭脸(脸都给我笑烂)c0df05f89c8946a292ad105e793e4010.jpeg#pic_center
至于为什么今天回突然想到写这个题呢,其实完全是因为今天的考试第一题要用Splay做,但我们机房一致决定舍弃Splay的方案,而转而使用这个更好理解且更实用 (更好背) 的无旋Treap来做,然而由于久而未敲平衡树的我们也并不是一项简单的事情。(对,到现在也还没有一个人过),于是先来想去就打算让今天的考试题随风去吧(回头再写),先来写写这个模板题。

就是这样啦,祝我们明天合唱比赛成功!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值