优先队列
基础
-
优先队列:队列中的元素保持某种优先关系,比如大根堆中的元素就是谁大谁在前。
-
朴素二叉堆:支持插入数,删除一个最值(堆顶),查询一个最值(堆顶)。
-
二叉堆本质是一颗完全二叉树,父亲总是比儿子拥有更高的优先级(比如更大、更小等)。堆顶元素优先级最高。
-
插入时把元素放在这棵树最底下(具体看代码),如果父亲比自己优先级低就交换,循环往复直至根节点。
-
删除堆顶类似,把堆顶与堆底交换后删除堆顶,此时堆顶就是优先级较低的元素。从左右儿子中挑一个优先级更高的比较,如果自己的优先级更低则与之交换,循环往复直至叶节点。
-
当然也可以直接用 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) (k−1) 大的元素都会被保留在小根堆中。如果来了一个比小根堆堆顶小的元素,则这个元素就是第 k k k 大的,将其扔进大根堆;否则前 ( k − 1 ) (k-1) (k−1) 大的元素就不包含此时小根堆堆顶的元素,将其弹出,也扔进大根堆。
从家到办公室和从办公室回家没有区别,所以直接把每个家和办公室看成每个独立的坐标,我们只需计算坐标到桥的距离即可。对于每个人,他走的总距离等于办公室到桥的距离+家到桥的距离+桥的长度。可以把桥的长度统一计算,把家和办公室到桥的距离拆开算。
观察到架的桥最多只有 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+L−1,st+R−1] 中前缀和的值最大的点作为结尾最优。
于是我们可以用一个 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,sumen−sumst−1→val。其中 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+L−1→l,st+R−1→r,计算出 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,en−1] 和 [ e n + 1 , r ] [en+1,r] [en+1,r] 两个元素,前提是扔进去的元素 l ≤ r l\leq r l≤r。取 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;
}