2.4
2.4.2 优先队列和堆
2.4.2.1优先队列
能够完成下列操作的数据结构叫做优先队列。
1.插入一个数值
2.取出最小的数值(获得数值,并且删除)
能够使用二叉树高效地解决上述问题地,是一种叫做堆的数据结构。
2.4.2.2 堆的结构
堆最重要的性质就是儿子的值一定不小于父亲的值。除此之外,树的节点是按从上到下,从左到右的顺序紧凑排列的。
在向堆种插入数值时,首先在堆的末尾插入该数值,然后不断向上提升直到父亲节点小于当前节点为止。
从堆中删除最小值时,首先把堆的最后一个节点的数值复制到根节点上,并且删除最后一个节点。然后不断向下交换直到没有大小颠倒为止
堆的操作的复杂度:堆的两种操作所化的时间都和树的深度成正比。因此,如果一共有n个元素,那么每个操作可以在O(logn)的时间内完成。
堆的实现:
1.左儿子的编号是自己的编号 x 2 + 1
2.右儿子的编号是自己的编号 x 2 + 2
int heap[maxn],sz=0;
void push(int x){
//自己节点的编号
int i=sz++;
while(i>0){
int p=(i-1)/2;//父亲节点的编号
if(heap[p]<=x)break;
heap[p]=heap[i];
i=p;
}
heap[i]=x;
}
int pop(){
int ret=heap[0];//最小值
int x=heap[--sz];
int i=0;
while(i*2+1<sz){
int a=i*2+1;
int b=i*2+2;
if(b<sz&&heap[b]<heap[a])a=b;//将节点往小的那边沉
if(heap[a]>=x)break;
heap[i]=heap[a];//将儿子的节点提上去
i=a;
}
heap[i]=x;
return ret;
}
编程语言的标准库:实际上,大部分情况下不需要自己实现堆。c++的STL里的priority_queue就是一个堆(这个堆默认的是最大值在前面)push()入堆,top()堆顶元素,pop()删除堆顶元素,empty()堆是否为空。
例题:POJ 2431 Expedition
由于加油站数量N非常大,必须想一个高效的解法。
我们稍微变换下思考方式。在卡车开往终点的途中,只有在加油站才可以加油。但是,如果认为“在到达加油站i时,就获得了一次在之后的任何时候都可以加Bi单位汽油的权利”,在解决问题上应该也是一样的。而在之后需要加油时,就认为是在之前经过的加油站加的油就可以了。
那么,因为希望到达终点时加油次数尽可能少,所以当燃料为0了之后再进行加油看上去是一个不错的方法,并且每次选择加油量最大的加油站。
1.在经过加油站i时,往优先队列里加入Bi
2.当燃料箱空了时,(1)如果优先队列也是空,则无法到达终点。(2)否则取出优先队列的最大元素,并用来给卡车加油。
int L,P,N;//L代表路程长度,P代表初始油量,N代表加油站个数
int A[maxn+1],B[maxn+1];
void solve(){
//为了方便,我们将终点L也设为加油站
A[N]=L;
B[N]=0;
priority_queue<int> pq;
int ans=0,pos=0,tank=p;//ans加油次数,pos当前位置,tank剩余油量
for(int i=0;i<N;i++){
int d=A[i]-pos;
while(d>tank){
if(pq.empty()){
puts("-1");
return;
}
tank+=pq.top();
pq.pop();
ans++;
}
tank-=d;
pq.push(B[i]);
pos=A[i];
}
cout << ans << endl;
}
Fence Repair 我们重写一下之前的贪心例题
我们只需要从板的集合里取出最短的两块,并且把长度为两块板长度之和的板加入集合中即可。O(nlogn)
typedef long long ll;
int N,L[maxn];
void solve(){
int ans=0;
priority_queue<int,vector<int>,greater<int>> que;//定义一个从小到大的值取出的优先队列
for(int i=0;i<N;i++)que.push(L[i]);
while(que.size()>1){
int t=0;
for(int i=0;i<2;i++){
ans+=que.top();
t+=que.top();
que.pop();
}
que.push(t);
}
cout << ans << endl;
}
2.4.3 二叉搜索树
2.4.3.1 二叉搜索树的结构
二叉搜索树是能够高效地进行如下操作的数据结构。
- 插入一个数值
- 查询是否包含某个数值
- 删除某个数值
所有的节点,都满足左子树上的所有节点都比自己的小,而右子树上的所有节点都比自己打这一条件。
删除节点一般需要根据下面三种情况分别进行处理: - 需要删除的节点没有左儿子,那么就把右儿子提上去。
- 需要删除的节点的左儿子没有右儿子,那么就把左儿子提上去。
- 如果两种情况都不满足,那么就把左儿子的子孙中最大的节点提到需要删除的节点上。
平均每次操作需要O(logn)的时间
以下是二叉搜索树的实现:
//表示节点的结构体
struct node{
int val;
node * lch,* rch;
};
node * insert(node * p,int x){
if(p==NULL){
node * q=new node;
q->val=x;
q->lch=q->rch=NULL;
return q;
}
else{
if(x<p->val)p->lch=insert(p->lch,x);
else p->rch=insert(p->rch,x);
return p;
}
}
bool find(node * p,int x){
if(p==NULL)return false;
if(p->val==x)return true;
if(p->val>x)return find(p->lch,x);
else return find(p->rch,x);
}
node * remove(node * p,int x){
if(p==NULL)return NULL;
if(x<p->val)p->lch=remove(p->lch,x);
else if(x>p->val)p->rch=remove(p->rch,x);
else if(p->lch==NULL){
node * q=p->rch;
delete p;
return q;
}
else if(p->lch->rch==NULL){
node * q=p->lch;
q->rch=p->rch;
delete p;
return q;
}
else {
node * q;
for(q=p->lch;q->rch->rch;q=q->rch){
;
}
node * r=q->rch;
r->rch=p->rch;
q->rch=NULL;
delete p;
return r;
}
return p;
}
编程语言的标准库
C++中,STL有set和map容器。set是像前面所说的一样使用二叉搜索树维护集合的容器,而map则是维护键和键对应的值的容器。set方法insert()添加元素,find()查找元素,count()也是查找元素,set<>::iterator it迭代器,begin()开始指针,end()结束后一位指针,erase()删除元素。
map方法,insert()插入元素(需要是一个pair对象)map<,>::iterator it迭代器,find()根据键查找对象,返回一个pair类型的迭代器对象。
2.4.4 并查集
1.并查集是什么
并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地进行如下操作。不过需要注意并查集虽然可以进行合并操作,但是无法进行分割操作。
- 查询元素a和元素b是否为同一组
- 合并元素a和元素b所在的组
2.并查集的结构
并查集也是用树形结构实现的。
每个元素对应一个节点,每个组对应一棵树。在并查集中,哪个节点是哪个节点的父亲节点以及形状等信息无需多加关注,整体组成一个树形结构才是重要的。
(1).初始化
我们准备n个节点来表示n个元素,最开始时没有边。
(2).合并
从一个组的根向另一个组的根连边,这样两棵树就变成了一棵树了。也就把两个组合并为一个组了。
(3)查询
为了查询两个节点是否属于同一组,我们需要沿着树往上走,来查询包含这个元素的树的根是谁。如果两个节点走到了同一个根,那么就可以知道它们属于同一个组。
3.并查集实现中的注意点
在树形数据结构中,如果发生了退化的情况,那么复杂度就会变得很高。并查集中,只需要按照这种方法就可以避免退化。
- 对于每棵树,记录这棵树的高度(rank)
- 合并时如果两棵树的rank不同,那么从rank小的rank大的连边。
此外通过路径压缩,可以使得并查集更加高效,对于每个节点一旦向上走到了一个根节点,就把这个点到父亲的边改为直接连向根。
在此之上,不仅仅使所查询的节点,在查询过程中向上所经过的所有的节点,都改为直接连到根上,这样再次查询这些节点时就很快知道根是谁了。
4.并查集的复杂度
O(α(n)),是O(logn)的反函数,比O(logn)还快。
5.并查集的实现
int par[maxn];
int rank[maxn];
void init(int n){
for(int i=0;i<n;i++){
par[i]=i;
rank[i]=0;
}
}
int find(int x){
if(par[x]==x)return x;
return par[x]=find(par[x]);//这里之所以要用par[x]=find(par[x])是为了路径压缩
}
void unite(int x,int y){
x=find(x);
y=find(y);
if(x==y)return;
if(rank[x]<rank[y]){
par[x]=y;
}else{
par[y]=x;
if(rank[x]==rank[y])rank[x]++;
}
}
bool same(int x,int y){//判断是否相等。
return find(x)==find(y);
}
例题:POJ 1182 食物链
对于每只动物i创建三个元素i-A,i-B,i-C,并用这3 x N元素建立并查集。这个并查集维护如下信息:
- i-x表示"i属于种类x"。
- 并查集中的每一个组表示组内所有元素代表的情况都同时发生或不发生。
例如,如果i-A和j-B在同一个组里,就表示如果i属于种类A那么j一定属于种类B,如果j属于种类B那么i一定属于种类A。因此,对于每一条信息,只需要按照下面进行操作就可以了。 - 第一种,x和y是同一种……合并x-A和y-A、x-B和y-B,x-C和y-C.
- 第二种,x吃y…………合并x-A和y-B、x-B和y-C、x-C和y-A。
不过在合并之前,需要先判断合并是否会产生矛盾。
int N,K;
int T[maxn],X[maxn],Y[maxn];
//这里省略了并查集的部分代码
void solve(){
//x,x+N,x+2N分别代表了x-A,x-B,x-C;
init(N*3);
int ans=0;
for(int i=0;i<K;i++){
int t=T[i];
int x=X[i]-1,y=Y[i]-1//把范围弄成0到N-1的范围
if(x<0||x>=N||y<0||y>=N){
ans++;
continue;
}
if(t==1){
if(same(x,y+N)||same(x,y+2*N)){//判断同时发生的情况,x是A组那么y为B组的情况,x是A组那么y为C组的情况。
ans++;
continue;
}
unite(x,y);
unite(x+N,y+N);
unite(x+2*N,y+2*N);
}
else{
if(same(x,y)||same(x,y+2*N)){//只需要判断两种情况就行了,一种是它们是同一种类的,另一种是它们中间间隔了一种情况。
ans++;
continue;
}
unite(x,y+N);
unite(x+N,y+2*N);
unite(x+2*N,y);
}
}
}