优先队列/二叉堆 算法解析+例题

优先队列


基础

  • 优先队列:队列中的元素保持某种优先关系,比如大根堆中的元素就是谁大谁在前。

  • 朴素二叉堆:支持插入数,删除一个最值(堆顶),查询一个最值(堆顶)。

  • 二叉堆本质是一颗完全二叉树,父亲总是比儿子拥有更高的优先级(比如更大、更小等)。堆顶元素优先级最高。

  • 插入时把元素放在这棵树最底下(具体看代码),如果父亲比自己优先级低就交换,循环往复直至根节点。

  • 删除堆顶类似,把堆顶与堆底交换后删除堆顶,此时堆顶就是优先级较低的元素。从左右儿子中挑一个优先级更高的比较,如果自己的优先级更低则与之交换,循环往复直至叶节点。

  • 当然也可以直接用 STL 中的 priority_queue,在此不作赘述。

  • 模板: P3378 【模板】堆

    int heap[maxn],siz; // heap 是堆,siz 是堆的大小。
    void push(int x) {
    	int now = siz + 1,nxt; heap[++ siz] = x; // 放在堆底。
    	while ((now >> 1) >= 1) {
    		nxt = now >> 1;
    		if (heap[nxt] > heap[now]) // 优先级。(此处为小的优先级更高)
    			heap[nxt] ^= heap[now],
    			heap[now] ^= heap[nxt],
    			heap[nxt] ^= heap[now]; // 这三条就是交换的意思,完全可以 swap(heap[nxt],heap[now]) 。
    		else break;
    		now = nxt;
    	}
    }
    void pop() {
    	heap[1] ^= heap[siz], heap[siz] ^= heap[1], heap[1] ^= heap[siz];
    	siz --; int now = 1, nxt; // 交换后删除此时的堆底。
    	while ((now << 1) <= siz) {
    		nxt = now << 1;
    		if (nxt + 1 <= siz && heap[nxt + 1] < heap[nxt]) nxt ++;
    		if (heap[nxt] < heap[now]) // 同上。
    			heap[nxt] ^= heap[now],
    			heap[now] ^= heap[nxt],
    			heap[nxt] ^= heap[now];
    		else break;
    		now = nxt;
    	}
    }
    // 查询最值就是 heap[1]。
    

应用场景

基础

直接模拟即可。

k k k 大/小

用权值线段树等都可以解决,但用堆解决更方便:对顶堆

顾名思义,两个顶堆顶的堆,通常为一个大根堆+一个小根堆。例如求第 k k k 大,则一开始把新加的元素扔进小根堆,此时如果小根堆的大小大于等于 k k k,就把小根堆的堆顶弹出扔进大根堆。此时大根堆的堆顶就是第 k k k 大的元素。

原理:因为一开始的元素都会被扔进小根堆,每次把最小的弹出扔进大根堆里,这样就保证了前 ( k − 1 ) (k-1) (k1) 大的元素都会被保留在小根堆中。如果来了一个比小根堆堆顶小的元素,则这个元素就是第 k k k 大的,将其扔进大根堆;否则前 ( k − 1 ) (k-1) (k1) 大的元素就不包含此时小根堆堆顶的元素,将其弹出,也扔进大根堆。

从家到办公室和从办公室回家没有区别,所以直接把每个家和办公室看成每个独立的坐标,我们只需计算坐标到桥的距离即可。对于每个人,他走的总距离等于办公室到桥的距离+家到桥的距离+桥的长度。可以把桥的长度统一计算,把家和办公室到桥的距离拆开算。

观察到架的桥最多只有 2 2 2 个。对于 1 1 1 个桥的情况直接对所有坐标取中位数架桥即可。

考虑从 1 1 1 个桥推出 2 2 2 个桥。一个可行的思路是枚举一个分界线,分界线左边的坐标点都走靠左的桥,分界线右边的坐标点都走靠右的桥,这样保证了每个坐标点总是走到离自己近的桥。

这样就拆成了两个一座桥的问题。将所有坐标点按 x x x 坐标排序,我们从左到右枚举分界线放在哪一个点的右边,动态计算出此时这些点的中位数和距离之和,这里就可以用对顶堆做,从而计算出每条分界线左边点到桥的距离之和。右边部分类似,只不过要从右往左枚举分界线放在哪一个点的右边。

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),注意特判办公室和家在同一侧的情况。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e6 + 5;
int k,nn,n = 0,pre = 0,ans = 0,point[maxn],m = 0; pair<int,int> b[maxn];
priority_queue<int,vector<int>,greater<int> > q2;
priority_queue<int> q1; int l[maxn],r[maxn];
char read() {
	char res = getchar();
	while (res < 'A' || res > 'Z') res = getchar();
	return res;
}
signed main() {
	scanf("%lld%lld",&k,&nn);
	char c1,c2;
	for (int i = 1ll,x,y;i <= nn;i ++) {
		c1 = read(); scanf("%lld",&x); c2 = read(); scanf("%lld",&y);
		if (c1 == c2) pre += abs(x - y);
		else pre ++, b[++ n] = {x,y}, point[++ m] = x, point[++ m] = y;
	}
	if (k == 1ll) {
		sort(point + 1ll,point + m + 1ll);
		int mid = point[m >> 1ll];
		for (int i = 1ll;i <= m;i ++)
			ans += abs(mid - point[i]);
		printf("%lld",ans + pre);
		return 0;
	}
	sort(b + 1ll,b + n + 1ll,[&](const pair<int,int> &x,const pair<int,int> &y) {
		return x.first + x.second < y.first + y.second;
	});
	// Part Left
	for (int i = 1ll,s1 = 0,s2 = 0,now,mid;i <= n;i ++) {
		q1.push(b[i].first); q1.push(b[i].second);
		s1 += b[i].first + b[i].second;
		s2 += q1.top(), s1 -= q1.top(), q2.push(q1.top()), q1.pop();
		if (q1.top() > q2.top()) {
			int u = q1.top(), v = q2.top();
			s1 = s1 - u + v, s2 = s2 - v + u;
			q1.pop(), q2.pop(), q1.push(v), q2.push(u);
		}
		l[i] = s2 - s1;
	}
	while (q1.size()) q1.pop(); while(q2.size()) q2.pop();
	// Part Right
	for (int i = n,s1 = 0,s2 = 0,now,mid;i >= 1ll;i --) {
		q1.push(b[i].first); q1.push(b[i].second);
		s1 += b[i].first + b[i].second;
		s2 += q1.top(), s1 -= q1.top(), q2.push(q1.top()), q1.pop();
		if (q1.top() > q2.top()) {
			int u = q1.top(), v = q2.top();
			s1 = s1 - u + v, s2 = s2 - v + u;
			q1.pop(), q2.pop(), q1.push(v), q2.push(u);
		}
		r[i] = s2 - s1;
	}
	ans = 1e18;
	for (int i = 1ll;i <= n + 1;i ++)
		ans = min(ans,l[i - 1ll] + r[i]);
	printf("%lld",ans + pre);
	return 0; 
}

综合运用(与二分/贪心相结合,都是紫题)

对于每一段超级和弦可能的起点 s t st st,考虑对原来音符美妙度求一个前缀和,则对于 s t st st 而言,找到 [ s t + L − 1 , s t + R − 1 ] [st+L-1,st+R-1] [st+L1,st+R1] 中前缀和的值最大的点作为结尾最优。

于是我们可以用一个 RMQ 快速求出一段区间内前缀和的值最大的点的编号。

接下来就是一个典型的套路:搞一个优先队列,每个元素包含和弦起点 s t st st、和弦结尾范围 [ l , r ] [l,r] [l,r]、这个元素能产生对答案的最大贡献的结尾 e n en en 和最大贡献 v a l val val v a l val val 大的元素靠前。设前缀和数组为 s u m sum sum,则 query ( l , r ) → e n , s u m e n − s u m s t − 1 → v a l \text{query}(l,r)\to en,sum_{en}-sum_{st-1}\to val query(l,r)en,sumensumst1val。其中 query(l,r) \text{query(l,r)} query(l,r) 表示查询 [ l , r ] [l,r] [l,r] 中的前缀和值最大的点的编号。

初始时枚举每一个可能的起点 s t st st,如果此时和弦合法则 s t + L − 1 → l , s t + R − 1 → r st+L-1\to l,st+R-1\to r st+L1l,st+R1r,计算出 v a l val val e n en en 后扔进优先队列。每次取出堆顶,处理完贡献后往堆中扔进 s t st st 相同、 [ l , r ] [l,r] [l,r] 分别为 [ l , e n − 1 ] [l,en-1] [l,en1] [ e n + 1 , r ] [en+1,r] [en+1,r] 两个元素,前提是扔进去的元素 l ≤ r l\leq r lr。取 k k k 次堆顶即可。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 500005; const int maxb = 28;
ll sum[maxn],ans; int n,k,L,R;
struct RMQ {
    ll st[maxn][maxb],log_2[maxn << 2];
    void pre() {
        for (int k = 0;k <= maxb;k ++) for (int i = (1 << k);i < (1 << (k + 1)) && i <= n;i ++) log_2[i] = k;
        for (int i = 1;i <= n;i ++) st[i][0] = i;
        for (int j = 1;j < maxb;j ++) for (int i = 1;i + (1 << j) - 1 <= n ;i ++) { st[i][j] = sum[st[i][j - 1]] > sum[st[i + (1 << (j - 1))][j - 1]] ? st[i][j - 1] : st[i + (1 << (j - 1))][j - 1]; }
    }
    int query(int l,int r) {
        int t = log_2[r - l + 1];
        return sum[st[l][t]] > sum[st[r - (1 << t) + 1][t]] ? st[l][t] : st[r - (1 << t) + 1][t];
    } 
}tab;
struct Node {
    int be,x,y,cur;
    Node (int a = 0,int b = 0,int c = 0) { be = a; x = b; y = c; cur = tab.query(x,y); }
    friend bool operator < (const Node &x,const Node &y) { return sum[x.cur] - sum[x.be - 1] < sum[y.cur] - sum[y.be - 1]; }
}; priority_queue<Node> q;
int main() {
    scanf("%d%d%d%d",&n,&k,&L,&R);
    for (int i = 1;i <= n;i ++) { scanf("%lld",&sum[i]); sum[i] += sum[i - 1]; }
    tab.pre(); for (int i = 1;i <= n;i ++) if (i + L - 1 <= n) q.push(Node(i,i + L - 1,min(i + R - 1,n))); 
    for (int i = 1,be,x,y,idx;i <= k;i ++) {
        be = q.top().be; x = q.top().x; y = q.top().y; idx = q.top().cur; 
        q.pop(); ans += sum[idx] - sum[be - 1]; 
        if (x != idx) q.push(Node(be,x,idx - 1));
        if (y != idx) q.push(Node(be,idx + 1,y));
    }
    printf("%lld",ans);
    return 0;
}

题解

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值