堆,可持久化线段树

堆,可持久化线段树

I.堆

堆是一颗完全二叉树

堆顶一定是堆中所有元素最大或最小的(对于优先级而言)

实现堆

应该不会有人手写堆吧

用STL的优先队列

定义一个优先队列:

首先你需要一个头文件:#include<queue>
priority_queue<int> q;//这是一个大根堆q
priority_queue<int,vector<int>,greater<int> >q;//这是一个小根堆q
//注意某些编译器在定义一个小根堆的时候greater<int>和后面的>要隔一个空格,不然会被编译器识别成位运算符号>>

优先队列的操作:

q.top()//取得堆顶元素,并不会弹出
q.pop()//弹出堆顶元素
q.push()//往堆里面插入一个元素
q.empty()//查询堆是否为空,为空则返回1否则返回0
q.size()//查询堆内元素数量

常用也就这些,貌似还有其他,不过基本也用不到,知道上面那几个也就可以了

不过有个小问题就是STL只支持删除堆顶,而不支持删除其他元素

但是问题不大,开一个数组del,在要删除其他元素的时候直接就标记一下del[i]=1,这里的下标是元素的值,然后在查询的时候碰到这个元素被标记了直接弹出然后继续查询就可以了

这里补一下重载运算符在STL的优先队列中应用到的知识

重载运算符是什么?

把一种运算符变成另外一种运算符(注意,都必须是原有的运算符),比如把<号重载成>号,这个东西学过STL中的sort的同学应该会比较熟悉

这个在优先队列中有什么用处呢?

之前我们就讲到了,大根堆,小根堆的“大”和“小”都不是传统意义下的“大”和“小”,重载运算符在STL的优先队列中就是用来解决这种“非传统意义的‘大’和‘小’”的

现在你有一个数列,它有权值和优先级两种属性,权值即该数的大小,优先级是给定的,现在要你按照优先级的大小从小到大输出这个数列

这不是Treap吗?这不是sort吗?

以上两个东西都可以用来实现这道题(逃,而且就实用性而言,sort用来解决这道题是最方便的,但是我们现在要讲的做法是使用堆排序的方式来解决这道题(堆排序是什么?下文堆的应用中有提到)

首先应该想得到结构体,我们定义一个结构体

struct node{
	int val,rnd;
}a[100];

但是使用传统做法是行不通的,在小根堆中是通过比较数的大小来确定各个元素在堆中的位置的,但是对于这个a数组,你是要对比权值val的值,还是要对比优先级rnd的值?

这时候重载运算符就派上用场了

我们在结构体里面再加3行东西

struct node{
	int val,rnd;
    bool operator < (const node&x) const {
		return rnd<x.rnd;
	}
}a[100];

这个玩意为什么要这么写呢?

首先这个玩意是bool类型的,因为你只需要判断这两个是大,还是小;然后,要重载运算符就必须加一个operator这个玩意,不然计算机怎么知道你要干嘛?后面接一个你要重载的运算符,这里是“<”,再后面的括号里面的东西则是你要比较的数据类型,这里是数据类型为node,并且加了一个指针&,将对这个x的修改同步到你实际上要修改的数据那里。然后就是记得加那两个const

然后两个大括号里面就是你重载的内容了,这里是把比较数的大小的小于号,重载成比较node这个数据类型里面的优先级的大小

没了(大雾)

II.可持久化线段树(主席树)

什么叫持久呢,“可持久化”定义:可以支持回退,访问之前版本的数据结构;支持回退操作的意思就是可以访问未经过其他操作的版本,也就是说返回到了以前的版本。由此我们可以知道主席树是基于线段树的一种数据结构

线段树与主席树的本质是一样的,只不过主席树可持久化,那么难点就在于怎么支持可持久化。

我们想要支持回退操作就可以对每一次修改操作都进行一次复制,将未进行操作的线段树版本进行复制,再对原线段树版本进行修改,那么我们就可以访问到旧版本的线段树了。不过现在问题来了,这样的空间复杂度将会乘上一个m,变成O(n*m)。

那我们来分析一下单点修改的线段树:主席树1

我们发现只有橙颜色经过的结点才被修改过。那么我们就可以思考,我们可不可以只对这些节点进行修改呢?答案当然是可以的,主席树的基本思想就是只对进行修改的结点进行复制。那么主席树是长什么样子的呢,下面一起来看一下吧。主席树2

我们可以从图中发现主席树的一些性质

1、每一次修改增加的节点个数为log(n)。

2、增加的非叶子结点会连向一个是其他版本的节点,一个是连向新节点。

3、主席树有很多根……

4、对于每一个根都可以构成一棵完整的线段树。

5、每一个节点都有可能有不只一个父节点……

所以我们可以知道主席树只会对部分节点进行复制,并且每一次复制的节点个数是log(n)。我们每一次想询问一个版本的线段树,就可以在那个版本的根构成的线段树中询问。

但同时也延伸出许多问题:

1、怎么构建新节点?怎么给新节点编号?怎么连边?

2、怎么访问子节点?

3、怎么存根?

很明显这些问题在线段树中完全不会出现,我们可以感觉到主席树在建树的代码中会和线段树不同。

现在给出刚才问题的答案:

1、直接开一块内存池存新节点。编号为此时总节点数个数+1。开结构体存子节点编号;线段树建什么边,一指了事。

2、访问子节点编号,不是像线段树一样乘2或乘2+1,而是在结构体存子节点编号。

3、另外开个数组存。

直接看代码吧(同样也不是本人写的)

所以我们定义一个节点要存三个信息:左儿子,右儿子,权值

struct node{
	int l, r, val;
}tree[N<<5];

新建节点:

inline int clone(int i){
	cnt++;
	tree[cnt]=tree[i];
	return cnt;
}

建树其实就是新建节点的过程:

inline int build(int i, int l ,int r)
{
	i = ++cnt;
	if(l == r)
	{
		tree[i].val = a[l];
	}
	if(l < r)
	{
		tree[i].l = build(tree[i].l,l,mid);
		tree[i].r = build(tree[i].r,mid+1,r);
	}
	return i;
}

更新和线段树很像:

inline int update(int i, int l, int r, int x, int val)
{
    i = clone(i);
    if(l == r)
    {
    	tree[i].val = val;
	}
    if (l < r){
        if (x <= mid) tree[i].l = update(tree[i].l, l, mid, x, val);
        else tree[i].r = update(tree[i].r, mid+1, r, x, val);
    }
    return i;
}

询问也一样:

inline int query(int v, int l, int r, int k)
{
	if(l == r)return tree[v].val;
	if(k <= mid)return query(tree[v].l, l, mid, k);
	return query(tree[v].r,mid+1, r, k);
}

那么主席树的操作部分就写完了QwQ

再来看主程序,里面看根怎么存储:

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(NULL);
	cout.tie(NULL);
	cin >> n >> m;
	for(int i = 1; i<= n; i++)
	{
		cin >> a[i];
	}
	date[0] = build(0, 1, n);
	for(int i = 1; i <= m; i++)
	{
		int x, y, z, j;
		cin >> x >> y >> z;
		if(y == 1)
		{
			cin >> j;
			date[i] = update(date[x], 1, n, z, j);
		}
		else
		{
			cout<<query(date[x], 1, n, z)<<"\n";
			date[i] = date[x];
		}
	}
}

(原谅我丑陋的代码)

posted @ 2025-04-05 10:36  HEGVDV  阅读(44)  评论(0)    收藏  举报