学习笔记:treap

最近在学一种数据结构:treap,这意味着我再也不用调用stl库的multiset怕常数太大超时了(当然,有的时候编程时间不够还是用stl库好一点)。

由于关于treap的详细而优秀的论文已经数不胜数,基本的操作就不说了。只是讲讲怎么编treap的问题。Treap我用三种方法各编了一次:不用指针,用指针但静态分配内存,指针加动态分配内存。这里讲一下前两种方法,第三种方法由于时间要比第二种方法慢一些,就不讨论了。

怎样不用指针写treap呢?老实说,有点麻烦。由于我们的lson和rson储存的都是空间池的编号,所以我们在进行左右旋某个节点的时候,以及删除它的时候都没有办法更改其父亲节点的lson或rson。对此,我们需要多一个附加域id来记录来记录某一个节点是否存在。比如:


如果是左边的情况,该节点是链节点,则把空间池中lson所在位置的信息赋到node的位置就好了,这样就可以看成fa直接到了lson这个节点。然而如果该节点是叶子节点,我们没有办法在空间池中删除这个位置,于是我们把该节点的id改为false。这样在每一次递归treap的时候要判断某一个节点是否有左儿子,不仅要看它的lson是否不为0,还要看左儿子的id是否为true。当然,我们也可以在递归的时候传多一个参数fa,来直接更改其fa所指向的值。

至于左右旋该怎么做,可以看一下下面的图:


比如现在我们要对黑色节点右旋。由于我们没有用指针,其fa记录的lson是空间池的编号3,并不知道3这个地方究竟储存的是哪一个节点,而我们要修改其fa的lson的值很麻烦。与其修改fa的lson为7,不如干脆先swap(e[3],e[7]),把黑色节点和它的左儿子在空间池的位置交换一下,然后再进行接下来的操作,这样会方便很多。

用非指针的treap写了一道小水题:洛谷的P1090合并果子……

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
#include<set>
#include<ctime>
using namespace std;

const int maxn=20010;

struct Tnode
{
	int lson,rson;
	int val,fix;
	bool id;
} tree[maxn];
int cur=-1;

int New_node(int v)
{
	cur++;
	tree[cur].lson=-1;
	tree[cur].rson=-1;
	tree[cur].val=v;
	tree[cur].fix=rand();
	tree[cur].id=true;
	return cur;
}

void Left_turn(int p)
{
	if (p)
	{
		int temp=tree[p].rson;
		swap(tree[p],tree[temp]);
		tree[temp].rson=tree[p].lson;
		tree[p].lson=temp;
	}
}

void Right_turn(int p)
{
	if (p)
	{
		int temp=tree[p].lson;
		swap(tree[p],tree[temp]);
		tree[temp].lson=tree[p].rson;
		tree[p].rson=temp;
	}
}

void Insert(int p,int v)
{
	bool fl=( tree[p].lson!=-1 && tree[ tree[p].lson ].id );
	bool fr=( tree[p].rson!=-1 && tree[ tree[p].rson ].id );
	if (v<tree[p].val)
	{
		if (fl) Insert(tree[p].lson,v);
		else tree[p].lson=New_node(v);
		if ( tree[ tree[p].lson ].fix < tree[p].fix ) Right_turn(p);
	}
	else
	{
		if (fr) Insert(tree[p].rson,v);
		else tree[p].rson=New_node(v);
		if ( tree[ tree[p].rson ].fix < tree[p].fix ) Left_turn(p);
	}
}

int Find(int p)
{
	bool fl=( tree[p].lson!=-1 && tree[ tree[p].lson ].id );
	if (fl) return Find(tree[p].lson);
	return tree[p].val;
}

void Delete(int p,int v)
{
	bool fl=( tree[p].lson!=-1 && tree[ tree[p].lson ].id );
	bool fr=( tree[p].rson!=-1 && tree[ tree[p].rson ].id );
	if (v==tree[p].val)
	{
		if ( fl && fr )
		{
			if (tree[ tree[p].lson ].fix<tree[ tree[p].rson ].fix)
			{
				Right_turn(p);
				Delete(tree[p].rson,v);
			}
			else
			{
				Left_turn(p);
				Delete(tree[p].lson,v);
			}
		}
		else if (fl) swap(tree[p],tree[ tree[p].lson ]);
			 else if (fr) swap(tree[p],tree[ tree[p].rson ]);
			 	  else tree[p].id=false;
	}
	else if (v<tree[p].val) Delete(tree[p].lson,v);
		 else Delete(tree[p].rson,v);
}

int main()
{
	freopen("c.in","r",stdin);
	freopen("c.out","w",stdout);
	
	srand(time(0));
	int root=New_node(0);
	
	int n;
	scanf("%d",&n);
	for (int i=1; i<=n; i++)
	{
		int a;
		scanf("%d",&a);
		Insert(root,a);
	}
	
	int sum=0;
	for (int i=1; i<n; i++)
	{
		int m1=Find(tree[root].rson);
		Delete(root,m1);
		int m2=Find(tree[root].rson);
		Delete(root,m2);
		Insert(root,m1+m2);
		sum+=(m1+m2);
	}
	
	printf("%d\n",sum);
	return 0;
}

如果不用指针的话,调试起来比较容易。指针的写法虽然比较难调(查看变量的时候你很可能会看到一大堆地址),但写熟了之后就会方便很多。因为用指针传递可以在对一个节点操作的时候顺带更改其父亲节点的lson或rson。

接下来说一下线段树和treap所能解决哪些题目,先来一道USACO的题:

挑剔的美食家

题目描述

与很多奶牛一样,Farmer John那群养尊处优的奶牛们对食物越来越挑剔,随便拿堆草就能打发她们午饭的日子自然是一去不返了。现在,Farmer John不得不去牧草专供商那里购买大量美味多汁的牧草,来满足他那N(1<= N <= 100,000)头挑剔的奶牛。 所有奶牛都对FJ提出了她对牧草的要求:第i头奶牛要求她的食物每份的价钱不低于A_i(1 <= A_i <= 1,000,000,000),并且鲜嫩程度不能低于B_i(1 <= B_i <= 1,000,000,000)。商店里供应M(1<= M <= 100,000)种不同的牧草,第i 种牧草的定价为C_i(1 <= C_i <= 1,000,000,000),鲜嫩程度为D_i(1 <= D_i <= 1,000,000,000)。 为了显示她们的与众不同,每头奶牛都要求她的食物是独一无二的,也就是说,没有哪两头奶牛会选择同一种食物。 Farmer John想知道,为了让所有奶牛满意,他最少得在购买食物上花多少钱。

输入格式

*第1行: 2个用空格隔开的整数:N 和 M
* 第2..N+1行: 第i+1行包含2个用空格隔开的整数:A_i、B_i * 第N+2..N+M+1行: 第j+N+1行包含2个用空格隔开的整数:C_i、D_i

输出格式

*第1行: 输出1个整数,表示使所有奶牛满意的最小花费。如果无论如何都无法 满足所有奶牛的需求,输出-1

输入样例

4 7
1 1
2 3
1 4
4 2
3 2
2 1
4 3
5 2
5 4
2 6
4 4

输出样例

12 
输出说明:
给奶牛1吃价钱为2的2号牧草,奶牛2吃价钱为4的3号牧草,奶牛3分到价钱
为2的6号牧草,奶牛4选择价钱为4的7号牧草,这种分配方案的总花费是12,为
所有方案中花费最少的。

其实这就是要求一个二维平面上,在某一个点的右上角的x值离它最近的点的x值是多少。对于这题,我们可以先把牧草按价钱排序,离散化,得到他们在线段树底层的位置是多少,然后按鲜美程度由大到小排序,如果是牧草就插入线段树,是奶牛就询问一次线段树,并把牧草所在位置的值更新为oo即可。于是用线段树完美解决这题。

然而这题很明显就是要考我们treap的,用平衡树代码非常简洁。那么是不是所有用treap做的东西都可以用线段树呢?答案是否定的。

我们注意到线段树是要做离散化的,这就意味着它要离线,于是我们把题目改成强制在线:第i头奶牛要求她的食物每份的价钱不低于A_i(1 <= A_i <= 1,000,000,000),且A_i等于输入的A_i^lastans,其中lastans是上一头奶牛的食物价钱(当然,这里的奶牛按B_i由大到小读入)。

好吧,我们还能用线段树吗?一开始离散化的时候我们对牧草和奶牛的价格排序,然而现在我们不知道A_i真正是多少,它可能是1到10^9,而我们不可能开这么大的线段树,所以还是老老实实用treap吧。

当然,我们知道线段树也能做treap所没法做的,比如说lazy操作……

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
#include<ctime>
using namespace std;

const int maxn=100100;
const int oo=1000000010;

struct Tnode
{
	Tnode *lson,*rson;
	int val,fix;
} tree[maxn];
int cur=-1;

struct data
{
	int A,B,id;
} grass[maxn<<1];

int n,m;
long long ans=0;
bool flag=true;

int temp;

bool Comp(data x,data y)
{
	return x.B>y.B || ( x.B==y.B && x.id>y.id );
}

Tnode *New_node(int v)
{
	cur++;
	tree[cur].lson=tree[cur].rson=NULL;
	tree[cur].val=v;
	tree[cur].fix=rand();
	return tree+cur;
}

void Right_turn(Tnode *&P)
{
	Tnode *W=P->lson;
	P->lson=W->rson;
	W->rson=P;
	P=W;
}

void Left_turn(Tnode *&P)
{
	Tnode *W=P->rson;
	P->rson=W->lson;
	W->lson=P;
	P=W;
}

void Insert(Tnode *&P,int v)
{
	if (!P) P=New_node(v);
	else
	{
		if (v<=P->val)
		{
			Insert(P->lson,v);
			if ( P->lson->fix <P->fix ) Right_turn(P);
		}
		else
		{
			Insert(P->rson,v);
			if ( P->rson->fix < P->fix ) Left_turn(P);
		}
	}
}

void Find(Tnode *&P,int v)
{
	if (!P) return;
	if ( P->val < v ) Find(P->rson,v);
	else
	{
		temp=min(temp,P->val);
		Find(P->lson,v);
	}
}

void Delete(Tnode *&P,int v)
{
	if ( P->val==v )
	{
		if ( P->lson && P->rson )
		{
			if ( P->lson->fix < P->rson->fix )
			{
				Right_turn(P);
				Delete(P->rson,v);
			}
			else
			{
				Left_turn(P);
				Delete(P->lson,v);
			}
		}
		else if (P->lson) P=P->lson;
			 else if (P->rson) P=P->rson;
			 	  else P=NULL;
	}
	else if ( P->val < v ) Delete(P->rson,v);
		 else if ( P->val > v ) Delete(P->lson,v);
}

int main()
{
	freopen("c.in","r",stdin);
	freopen("c.out","w",stdout);
	
	srand(time(0));
	scanf("%d%d",&n,&m);
	
	if (n>m) flag=false;
	else
	{
		for (int i=1; i<=n; i++)
		{
			scanf("%d%d",&grass[i].A,&grass[i].B);
			grass[i].id=0;
		}
		for (int i=n+1; i<=n+m; i++)
		{
			scanf("%d%d",&grass[i].A,&grass[i].B);
			grass[i].id=1;
		}
		
		sort(grass+1,grass+n+m+1,Comp);
		
		Tnode *root=NULL;
		for (int i=1; i<=n+m; i++)
		{
			if (grass[i].id) Insert(root,grass[i].A);
			else
			{
				temp=oo;
				Find(root,grass[i].A);
				if (temp==oo)
				{
					flag=false;
					break;
				}
				Delete(root,temp);
				ans+=(long long)temp;
				
				//printf("%d\n",ans);
			}
		}
	}
	
	if (!flag) printf("-1\n");
	else printf("%lld\n",ans);
	
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值