堆和 priority_queue

一、堆的定义

堆(heap),是一棵有着特殊性质的完全二叉树,可以用来实现优先级队列(priority queue)。
堆需要满足以下性质:
1. 是一棵完全二叉树;
2. 对于树中每个结点,如果存在子树,那么该结点的权值大于等于(或小于等于)子树中所有结点
的权值。
如果根结点大于等 于子树结点的权值,称为大根堆;反之,称为小根堆。
二、堆的存储
由于堆是一个完全二叉树,因此我们可以用一个数组来存储。
当结点下标为 i 时
如果父存在,父下标为 i/2 ;
如果左孩子存在,左孩子下标为 i × 2 ;
如果右孩子存在,右孩子下标为 i × 2 + 1 。
这样存储固然简单,但是题目不会那么好心,直接给我们一个标准的堆。一般给我们的是一组数,这组数按照给出的顺序还原成二叉树之后,并不是一个堆结构。此时如果想将这组数变成堆的话,有两种操作:
1. 用数组存下来这组数,然后把数组调整成一个堆;
2. 创建一个堆,然后将这组 数依次插入到堆中。
现在可能会有点迷糊,但看下去就会明白了。
三、 核心操作
堆中的所有运算,比如建堆,向堆中插入元素以及删除元素等,都是基于堆中的两个核心操作实现的——向上调整算法以及向下调整算法。因此,在实现堆之前,我们先来掌握两种核心操作。注意:以下所有操作都默认堆是一个大根堆,小根堆的原理反着来即可。
1.向上调整算法
算法流程:
(1)与父结点的权值作比较,如果比它大,就与父亲交换;
(2)交换完之后,重复 1 操作,直到比父亲小,或者换到根节点的位置。
#include <iostream>
using namespace std;
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
int main() 
{
	
	return 0;
}

2.向下调整算法

算法流程:
(1)找出左右儿子中权值最大的那个,如果比这个值小,就与其交换;
(2)交换完之后,重复 1 操作,直到比儿子结点的权值都大,或者换到叶节点的位置。
#include <iostream>
using namespace std;
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
void down(int parent)
{
	int child=parent*2;
	while(child<=n)
	{
		if(child+1<=n&&heap[child+1]>heap[child])child++;
		if(heap[child]<=heap[parent])return;
		swap(heap[child],heap[parent]);
		parent=child;
		child=parent*2;
	}
}
int main() 
{
	
	return 0;
}

四、堆的模拟实现

1.创建
const int N=1e6+10; 
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆

2.插入

把新来的元素放在最后一个位置,然后从最后一个位置开始执行一次向上调整算法即可。

#include <iostream>
using namespace std;
const int N=1e6+10; 
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
//向下调整算法
void down(int parent)
{
	int child=parent*2;
	while(child<=n)
	{
		if(child+1<=n&&heap[child+1]>heap[child])child++;
		if(heap[child]<=heap[parent])return;
		swap(heap[child],heap[parent]);
		parent=child;
		child=parent*2;
	}
}
//插入
void push(int x)
{
	heap[++n]=x;
	up(n);
}
void  
int main() 
{
	
	return 0;
}

3.删除栈顶元素

(1)将栈顶元素和最后一个元素交换,然后 n--,删除最后一个元素;
(2)从根节点开始执行一次向下调整算法即可。
#include <iostream>
using namespace std;
const int N=1e6+10; 
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
//向下调整算法
void down(int parent)
{
	int child=parent*2;
	while(child<=n)
	{
		if(child+1<=n&&heap[child+1]>heap[child])child++;
		if(heap[child]<=heap[parent])return;
		swap(heap[child],heap[parent]);
		parent=child;
		child=parent*2;
	}
}
//插入
void push(int x)
{
	heap[++n]=x;
	up(n);
}
//删除栈顶元素
void pop()
{
	swap(heap[1],heap[n]);
	n--;
	down(1);
}
void  
int main() 
{
	
	return 0;
}

4.堆顶元素

下标为 1 位置的元素,就是堆顶元素。
#include <iostream>
using namespace std;
const int N=1e6+10; 
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
//向下调整算法
void down(int parent)
{
	int child=parent*2;
	while(child<=n)
	{
		if(child+1<=n&&heap[child+1]>heap[child])child++;
		if(heap[child]<=heap[parent])return;
		swap(heap[child],heap[parent]);
		parent=child;
		child=parent*2;
	}
}
//插入
void push(int x)
{
	heap[++n]=x;
	up(n);
}
//删除栈顶元素
void pop()
{
	swap(heap[1],heap[n]);
	n--;
	down(1);
}
//堆顶元素
int top()
{
	return heap[1];
}
int main() 
{
	
	return 0;
}

5.堆的大小

#include <iostream>
using namespace std;
const int N=1e6+10; 
int n; // 标记堆的大小 
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
	int parent =child/2;
	while(parent>=1&&heap[child]>heap[parent])
	{
		swap(heap[child],heap[parent]);
		child=parent;
		parent=child/2;
	}
}
//向下调整算法
void down(int parent)
{
	int child=parent*2;
	while(child<=n)
	{
		if(child+1<=n&&heap[child+1]>heap[child])child++;
		if(heap[child]<=heap[parent])return;
		swap(heap[child],heap[parent]);
		parent=child;
		child=parent*2;
	}
}
//插入
void push(int x)
{
	heap[++n]=x;
	up(n);
}
//删除栈顶元素
void pop()
{
	swap(heap[1],heap[n]);
	n--;
	down(1);
}
//堆顶元素
int top()
{
	return heap[1];
}
//堆的大小
int size()
{
	return n;	
}  
int main() 
{
	
	return 0;
}

五、priority_queue

1.优先级队列

普通的队列是一种先进先出的数据结构,即元素插入在队尾,而元素删除在队头。而在优先级队列中,元素被赋予优先级,当插入元素时,同样是在队尾,但是会根据优先级进行位置调整,优先级越高,调整后的位置越靠近队头;同样的,删除元素也是根据优先级进行,优先级最高的元素(队头)最先被删除。
其实我们可以认为,优先级队列就是堆实现的一个数据结构。priority_queue 就是 C++ 提供的,已经实现好的优先级队列,底层实现就是一个堆结构。在算法竞赛中,如果是需要使用堆的题目,我们一般就直接用现成的 priority_queue,很少手写一个堆,因为这样比较省事。
2.创建 priority_queue
优先级队列的创建结果有很多种,因为需要根据实际需求,可能会创建出来各种各样的堆:
(1)简单内置类型的大根堆或小根堆:比如存储 int 类型的大根堆或小根堆;
(2)存储字符串的大根堆或小根堆;
(3)存储自定义类型的大根堆或小根堆:比如堆里面的数据是一个结构体。
关于每⼀种创建结果,都需要有与之对应的写法。在初阶阶段,我们先用简单的 int 类型建堆,重点学习priority_queue 的用法。
注意: priority_queue 包含在 queue 这个头文件中。
我们先用 priority_queue来创建一个堆。
#include <iostream>
#include <queue>
using namespace std;
int main() 
{
	priority_queue<int> heap;// 默认写法下,是一个大根堆
	int a[10]={ 1, 41, 23, 10, 11, 2, -1, 99, 14, 0};
	return 0;
}

(1)size / empty

size :返回元素的个数。empty :返回优先级队列是否为空

(2)push

往优先级队列里面添加一个元素。

(3)pop

删除优先级最高的元素。

(4)top

获取优先级最高的元素。
#include <iostream>
#include <queue>
using namespace std;
int main() 
{
	priority_queue<int> heap;// 默认写法下,是一个大根堆
	int a[10]={ 1, 41, 23, 10, 11, 2, -1, 99, 14, 0};
	for(int i=0;i<10;i++)
	{
		heap.push(a[i]);
	}
	while(heap.size())
	{
		cout << heap.top() << " ";
		heap.pop();
	}
	return 0;
}

从运行结果来看,为降序,也验证了我们创建出的是大根堆。

3.priority_queue的内置类型

内置类型就是 C++ 提供的数据类型,比如 int double long long 等。以 int 类型为例,分别创建大根堆和小根堆。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main() 
{
	priority_queue<int> heap1;//默认就是大根堆 
	// priority_queue<数据类型, 存数据的结构, 数据之间的比较方式>
    priority_queue<int, vector<int>, less<int>> heap2; //大根堆
    priority_queue<int, vector<int>, greater<int>> heap3; // 小根堆
	return 0;
}

4.priority_queue的结构体类型

当优先级队列里面存的是结构体类型时,需要在结构体中重载 < 比 较运算符,从而创建出大根堆 或者小根堆。
注:这里的主要目的是把优先级队列创建出来,因此只用掌握写法即可。关于其中背后的原理,这里就不再赘述,因为涉及比较复杂的 C++ 语法知识。在这里,学习的侧重点就是模仿。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
struct node
{
    int a, b, c;
    // // 以 b 为基准,定义大根堆
    // bool operator < (const node& x) const
    // {
    // return b < x.b;
    // }
    // 以 b 为基准,定义小根堆
    bool operator < (const node& x) const
    {
    return b > x.b;
    }
};
int main() 
{
    priority_queue<node> heap;
    for(int i = 1; i <= 10; i++)
    {
       heap.push({i, i + 1, i + 2});
    }
    while(heap.size())
    {
       auto t = heap.top();
       heap.pop();
       cout << t.a << " " << t.b << " " << t.c << endl;
    }
	return 0;
}

在C++中,std::priority_queue默认是一个大根堆。这意味着如果没有自定义比较函数或重载operator<,std::priority_queue会将较大的元素视为更高的优先级。但因为我们已经通过重载operator<改变了比较规则,使得std::priority_queue基于自定义规则工作。在这个实现中,如果当前节点的b值大于另一个节点的b值,则认为当前节点“小于”另一个节点。这种逆向的比较逻辑意味着具有较小b值的节点会被赋予更高的优先级,因此它实际上表现得像一个小根堆。

六、相关的算法题
1.
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
typedef long long LL;
priority_queue<LL,vector<LL>,greater<LL>> q;
int n,op;
LL x;
int main() 
{
	cin >> n;
	while(n--)
	{
		cin >> op;
		if(op==1)
		{
			cin >> x;
			q.push(x);
		}
		else if(op==2)
		{
			cout << q.top() << endl;
		}
		else
		{
			q.pop();
		}
	}
	return 0;
}

2.

#include <iostream>
#include <queue>
using namespace std;
int n, m, k, item, op;
priority_queue<int> q;
int main()
{
	cin >> n >> m >> k;
	while (n--)
	{
		cin >> item;
		q.push(item);
		if (q.size() > k)q.pop();
	}
	while (m--)
	{
		cin >> op;
		if (op == 1)
		{
			cin >> item;
			q.push(item);
			if (q.size() > k)q.pop();
		}
		else
		{
			if (k > q.size())
				cout << -1 << endl;
			else
				cout << q.top() << endl;
		}
	}
	return 0;
}

3.

#include <iostream>
#include <queue>
using namespace std;
priority_queue<int> heap;
int i;
long long sum;
int main() 
{
	int n,k,item;
	cin >> n >> k;
	while(n--)
	{
		cin >> item;
		heap.push(item);
	}
	while(heap.size()&&i<k)
	{
		if(heap.top()%2==1)
		{
			sum+=heap.top();
			heap.pop();
		}
		else
		{
			int u=heap.top();
			heap.pop();
			heap.push(u/2);
			i++;
		}
	}
	int sz=heap.size();
	while(sz--)
	{
		sum+=heap.top();
		heap.pop();
	}
	cout << sum << endl;
	return 0;
}

4.

#include <iostream>
#include <queue>
using namespace std;
const int N=1e4+10;
int a[N],b[N],c[N],sum;
struct node
{
	int sum;//计算结果 
	int u;//第u组 
	int x;//x取值
	bool operator<(const node& q) const
	{
		return sum>q.sum;	
	} 
};
int calc(int l,int r)
{
	return a[l]*r*r+b[l]*r+c[l];
}
priority_queue<node> heap;
int main() 
{
	int n,m;
	cin >> n >> m;
	for(int i=1;i<=n;i++)
	{
		cin >> a[i] >> b[i] >> c[i];
		heap.push({calc(i,1),i,1});
	}
	while(m--)
	{
		auto v=heap.top();
		cout << v.sum << " ";
		int sum=v.sum;int u=v.u;int x=v.x;
		heap.pop();
		heap.push({calc(u,x+1),u,x+1});
	}
	return 0;
}

5.

#include <iostream>
#include <queue>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
struct node
{
	int sum;
	int a;
	int b;	
	bool operator<(const node& x) const
	{
		return sum>x.sum;
	}
};
priority_queue<node> heap;
int main() 
{
	int n;
	cin >> n;
	for(int i=1;i<=n;i++)
	{
		cin >> a[i];
	}
	for(int i=1;i<=n;i++)
	{
		cin >> b[i];
	}
	for(int i=1;i<=n;i++)
	{
		heap.push({a[i]+b[1],i,1});
	}
	while(n--)
	{
		auto u=heap.top();
		cout << u.sum << " ";
		int sum=u.sum;int x=u.a;int y=u.b;
		heap.pop();
		heap.push({a[x]+b[y+1],x,y+1});
	}
	return 0;
}

6.

#include <iostream>
#include <queue>
#include <vector>
#include <cmath>
using namespace std;
struct node
{
	int f;
	int l;
	int r;
	bool operator<(const node& x) const
	{
		if(f != x.f) return f > x.f;
		else if(l != x.l) return l > x.l;
 		else return r > x.r;
	}
};
const int N=2e5+10;
int n;
char item;
int s[N];
int e[N],ne[N],pre[N];
priority_queue<node> heap;
vector<node> ret;
bool st[N];
int main() 
{
	cin >> n;
	for(int i=1;i<=n;i++)
	{
		cin >> item;
		if(item=='B')s[i]=1;
	}
	for(int i=1;i<=n;i++)
	{
		cin >> e[i];
		pre[i]=i-1;
		ne[i]=i+1;
	}
	ne[n]=0;
	for(int i=2;i<=n;i++)
	{
		if(s[i]!=s[i-1])heap.push({abs(e[i]-e[i-1]),i-1,i});
	}
	while(heap.size())
	{
		node t = heap.top(); heap.pop();
		int f = t.f, l = t.l, r = t.r;
		if(st[l] || st[r]) continue;
    	ret.push_back(t);
    	st[l] = st[r] = true; // 标记 l 和 r 已经出列
    	// 修改指针,还原新的队列
    	ne[pre[l]] = ne[r];
    	pre[ne[r]] = pre[l];
    	// 判断新的左右是否会成为对
    	int left = pre[l], right = ne[r];
    	if(left && right && s[left] != s[right])
    	{
    		heap.push({abs(e[left] - e[right]), left, right});
    	}
    }
    cout << ret.size() << endl;
    for(auto& x : ret)
    {
    	cout << x.l << " " << x.r << endl;
    }
	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值