堆 学习笔记

堆总是一棵完全树,堆中某个节点的值总是不大于或不小于其父节点的值。
先上一道堆的模板题(小根堆)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;
}

堆也是树形数据结构之一,作为工具蛮好用,可以练练

猜你喜欢

转载自blog.csdn.net/qq_43455647/article/details/89397182