堆总是一棵完全树,堆中某个节点的值总是不大于或不小于其父节点的值。
先上一道堆的模板题(小根堆)P3378 【模板】堆 ,在不用stl的前提下手写堆,其实就是通过插入和删除来维护堆。具体操作见代码。(因为用C语言提交,多写了一个swap规则)
#define swap(a, b) swap_temp=a;a=b;b=swap_temp
void Insert(int x)
{
int now, nex;
heap[++heap_size] = x; //插入堆尾
now = heap_size; //标记为当前节点
while(now > 1) //一步步向上回溯,知道回到1
{
nex = now >> 1; //移动到父节点
if(heap[now] >= heap[nex]) //如果上不去了,说明上堆都比下堆小,结束
return;
swap(heap[now], heap[nex]); //不然就交换点的位置
now = nex; //移动当前节点
}
}
void Delete() //其实是Insert的逆过程
{
int now, nex;
heap[1] = heap[heap_size--]; //先把最大的放到堆顶,并size--
now = 1; //从1开始向下维护堆,Insert是从底维护向上
while(now*2 <= heap_size)
{
nex = now << 1;
if(nex < heap_size && heap[nex+1] < heap[nex]) //同一个父节点的两个儿子互相比较
nex ++;
if(heap[now] <= heap[nex])
return;
swap(heap[now], heap[nex]);
now = nex;
}
}
我甚至拿这个手写堆交了次快速排序的板子,惊奇的发现,时间和空间都优于C++的stl中的sort,当然这应该是因为我拿C语言提交的优势,但是这绝对也是一个优秀的排序算法。
但是一般来说都是有STL库中的priority_queue(优先队列),基本上用不到手写,下面是最近练的几个例题。
P1090 合并果子
最入门,从优先队列中弹出一个处理后再压进队列
P1631 序列合并
求一堆数中的最小n个数,算是堆的基本操作,这个题需要一点点小优化就能过,总的来说不是什么难题。
P2085 最小函数值
这题和序列合并基本上师出同源,做了上题这题基本上改改就过
P1168 中位数
这题需要两个堆,一个大堆一个小堆,通过维护两个堆得到中位数
以上四题都是练手的水题(其实真的很水),接下来开始表演堆的进阶用法、
P2278 [HNOI2003]操作系统
这题教你操作堆,不会也得会
int n;
ll t;
struct node
{
int id, start, last, Rank;
bool operator < (const node &x) const //定义结构内判断规则
{
if(x.Rank == Rank) return start > x.start;
else return Rank < x.Rank;
}
}b;
priority_queue<node> q;
int main()
{
while(scanf("%d %d %d %d", &b.id, &b.start, &b.last, &b.Rank)==4)
{
while(!q.empty() && t+q.top().last <= b.start)
{
node k = q.top(); q.pop();
printf("%d %d\n", k.id, t+k.last);
t += k.last; //时间一直随着更新
}
if(!q.empty())
{
node k = q.top(); q.pop();
k.last = k.last - (b.start - t); //在不满足上述循环情况下,队首元素的last的时间会减短,更新
q.push(k);
}
q.push(b);
t = b.start;
}
while(!q.empty())
{
node k = q.top(); q.pop();
t += k.last;
printf("%d %lld\n", k.id, t);
}
return 0;
}
P1484 种树
所谓用堆来解决问题,从来就是一个词,贪心,堆的使用是否成功取决于你贪心的策略,而贪心一般来说处理起全局最优是有难度的,本题引入了一个小有名气的进阶贪心策略,反悔贪心。
通过读题,我们发现贪心有一个尴尬之处在于,你如果选择了此刻的最大值i,那么在选择两个的条件下,
你只能选择a[j]+a[i] (j !=i-1 && j != i+1)
,
但是这无法保证一定存在 a[i]+a[j] > a[i-1]+a[i+1]
,这种时候怎么处理呢?
我们想,选择a[i]和选择a[i-1]+a[i+1]一定是不兼容的,所以我们可以计算一个反悔值a[i-1]+a[i+1]-a[i],压入队列,如果后面计算时能够调用它自然会替换之前的a[i],注意压入前要更新反悔节点左右的关系,方便做之后的反悔节点
ll ans;
int n, m, a[maxn];
int l[maxn], r[maxn]; //一个表示左边一个表示右边,用数组是因为后面会有更新
bool vis[maxn];
struct node
{
int x, id;
bool operator < (const node &k) const
{return x < k.x;} //排序规则
};
priority_queue<node> q;
int main()
{
n = read(); //自己写过的快读
m = read();
FOR(i, 1, n)
{
a[i] = read();
l[i] = i-1;
r[i] = i+1;
q.push((node){a[i], i});
}
l[n+1] = n; r[0] = 1;
FOR(i, 1, m)
{
while(vis[q.top().id])
q.pop(); //把多余的元素丢了
node k = q.top();
q.pop();
if(k.x < 0)
break;
ans += k.x;
int kid = k.id;
a[kid] = a[l[kid]] + a[r[kid]] - a[kid]; //反悔元素,核心!
k.x = a[kid];
vis[r[kid]] = vis[l[kid]] = 1; //标记,节点k周围的不能用了
l[kid] = l[l[kid]]; //更新反悔节点的左右关系
r[kid] = r[r[kid]];
r[l[kid]] = kid;
l[r[kid]] = kid;
q.push(k); //把反悔标记塞进去
}
printf("%lld", ans);
return 0;
}
堆也是树形数据结构之一,作为工具蛮好用,可以练练