数据结构·堆
堆
堆是一种数据结构。没了
堆是一种树形结构,堆顶始终保持为所有元素的最优值,所以常常运用于贪心中。
大根堆的根为堆的最大值,小根堆的根为堆的最小值。堆一般用二叉树实现。
【YbtOj】题解
A.合并果子
可以贪心地想到每次选取代价最小的两堆进行合并,再将新的一堆放入所有待选堆中。每次操作用小根堆维护最小代价即可,复杂度\(O(n\ log\ n)\)。
关于\(O(n)\)的做法:我们可以想到,对于新合并成的一堆果子,其代价一定是单调递增的,于是可以用另一个队列存储新合成的果子堆,每次操作取原队列与第二个队列的队头较小值,这样不用堆便维护了队列的单调性。为了确保\(O(n)\)的复杂度,刚开始直接上简单粗暴的桶排即可 。
B.序列合并
用大根堆维护目前所选数的最大值,因为给出序列已具有单调性,所以直接for循环两个指针分别从前往后分别扫两个序列即可。若目前没选够\(n\)个数,那么直接插入\(a_{i}+b_{j}\);若已选够,那么就与目前最大值比较判断是否更优。均摊下来复杂度为\(O(n\ log\ n)\),不会炸(此处存疑)
AC代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n;
int a[N],b[N];
priority_queue <int> q;
stack <int> s;
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
for (int i=1;i<=n;i++) scanf("%lld",&b[i]);
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
int t=a[i]+b[j];
if (q.size()<n) { q.push(t); continue; }
if (t<q.top()) { q.pop(); q.push(t); continue; }
break;
}
}
while (!q.empty()) { s.push(q.top()); q.pop(); }
while (!s.empty()) { printf("%lld ",s.top()); s.pop(); }
return 0;
}
C.龙珠游戏
看到神似“删除”的操作,想到链表。用大根堆维护目前队列的最大值,若当前堆顶\(i\)没有走过且不是队列最后一个元素,那么与\(nxt_{i}\)一同出队,并链表叽里呱啦操作一下、标记已出队即可。
来自sxht dalao:不用堆优化就能做到\(O(n)\),因为\(a\)中元素两两不同,根据\(a_{i}\)建链表即可。
AC代码(带无意义的堆优化)
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
using namespace std;
const int N=1e5+5;
int n;
int a[N];
int nxt[N],pre[N];
bool flag[N];
priority_queue <pii> q;
queue <int> ans;
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) { scanf("%lld",&a[i]); q.push(mp(a[i],i)); }
for (int i=1;i<n;i++) nxt[i]=i+1;
for (int i=2;i<=n;i++) pre[i]=i-1;
while (ans.size()<n)
{
while (!q.empty()&&(flag[q.top().second]||nxt[q.top().second]==0)) q.pop();
int t1=q.top().second,t2=nxt[t1];
flag[t1]=true,flag[t2]=true;
pre[nxt[t2]]=pre[t1],nxt[pre[t1]]=nxt[t2];
ans.push(a[t1]),ans.push(a[t2]);
q.pop();
}
while (!ans.empty()) { printf("%lld ",ans.front()); ans.pop(); }
return 0;
}
D.工作安排
很典的一道反悔贪心。为了能完成尽量多的工作,我们每次肯定是取截止时间更靠前的工作;但若是有一个工作靠前,但获利少,而后面存在一个获利更多的工作,这样的贪心选择就会不那么优。于是我们会想到,每次对于一个获利更多但没时间的工作,将已选的工作中获利最少的替换掉,这样一定是更优的。
所以,刚开始按照截止日期升序排序,用小根堆维护目前已选的最小的 \(P_{i}\) 就行。至于选了多少个数?堆内元素数量就是选的数的个数
AC代码
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n;
struct node{
int d,p;
}a[N];
priority_queue < pii,vector <pii>,greater<pii> > q;
int ans;
bool cmp(node x,node y) { return x.d<y.d; }
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld%lld",&a[i].d,&a[i].p);
sort(a+1,a+1+n,cmp);
for (int i=1;i<=n;i++)
{
if (q.size()<a[i].d)
{
ans+=a[i].p;
q.push({a[i].p,a[i].d});
}
else
{
if (a[i].p>q.top().first)
{
ans=ans-q.top().first+a[i].p;
q.pop();
q.push({a[i].p,a[i].d});
}
}
}
printf("%lld",ans);
return 0;
}
E.家庭作业
也是很典的一道题呢(让我想起来了一个月前让我痛苦万分的分冰棍!)
先按照截止时间排序,每次尽量不喝奶茶,若对于当前 \(d_{i}\) 无法满足,那么前面就一定要喝奶茶了。为了尽量少喝,我们就要选性价比最高的奶茶,用大根堆维护即可;而喝奶茶不能喝到“时光倒流”,所以还需要维护每种奶茶的剩余量。
AC代码
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,double>
#define mp make_pair
using namespace std;
const int N=2e5+5;
int n;
struct node{
int l,t,d;
}a[N];
int tme;
double ans;
priority_queue < pii > q;
bool cmp(node x,node y) { return x.d<y.d; }
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld%lld%lld",&a[i].l,&a[i].t,&a[i].d);
sort(a+1,a+1+n,cmp);
for (int i=1;i<=n;i++)
{
q.push(mp(a[i].l,1.0*a[i].t));//又多了一种奶茶
tme+=a[i].t;
while (tme>a[i].d)//还得喝奶茶
{
int fst=q.top().first,d=tme-a[i].d;
double scd=q.top().second;
q.pop();
if (scd>=d)//要补的
{
ans+=1.0*d/(1.0*fst);
if (scd!=d) q.push(mp(fst,scd-1.0*d));
tme=a[i].d;
break;
}
else//不够
{
tme-=scd;
ans+=scd/(1.0*fst);
}
}
}
printf("%.2lf",ans);
return 0;
}
F.选数游戏
若选了第 \(i\) 列的数,那么它有两种贡献方式:一是使得可以到达 \(i\) 后贡献更大的列,二是它的值直接贡献。因此,若一列有取到的数,一定是只选一个或全都选。
于是,从左到右遍历数列,遍历到第 \(i\) 列时,从第 \(1\) 到 \(i\) 列一定都有被选的数,此时 \(sum\) 相当于是 选到第 \(i\) 列的最大贡献和,所有 \(sum\) 的最大值就是最终答案。
到第 \(i\) 列时,将其所有的数都计入 \(cnt\) 与 \(sum\) ,若溢出,那么每次去除最小值即可,用小根堆维护。
AC代码
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e6+5;
int n,m,k;
int a[N];
priority_queue < int,vector <int>,greater<int> > q;
int cnt,sum;
int ans;
signed main()
{
scanf("%lld%lld%lld",&n,&m,&k);
for (int i=1;i<=m;i++) scanf("%lld",&a[i]);
int maxx=0;
for (int i=1;i<=min(m,k);i++)
{
q.push(a[i]);
cnt+=n,sum+=n*a[i];
while (cnt>k&&!q.empty())
{
cnt-=n-1,sum-=(n-1)*q.top();
maxx=q.top();
q.pop();
}
ans=max(ans,sum+min(n,k-cnt)*maxx);
}
printf("%lld",ans);
return 0;
}
G.内存管理
直接模拟即可。小根堆维护当前编号最小的未分配内存块,队列维护被分配的内存块顺序, \(t_{i}\) 维护每个已分配内存块最后被操作的时间,每次操作将不合法的编号出队即可。
AC代码
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
#define fst first
#define scd second
using namespace std;
const int N=3e4+5;
int t[N];//对于被占用的内存块i,最后一次被操作的时间
priority_queue < int,vector <int>,greater<int> > q;
queue <pii> out;
signed main()
{
for (int i=1;i<=30000;i++) q.push(i);
int x,y;
char c;
while (cin>>x>>c)
{
while (!out.empty()&&out.front().scd<=x-600)
{
if (out.front().scd==t[out.front().fst])
{
t[out.front().fst]=0;
q.push(out.front().fst);
}
out.pop();
}
if (c=='+')
{
cout<<q.top()<<'\n';//分配编号最小的空闲的
t[q.top()]=x;
out.push(mp(q.top(),x));
q.pop();
}
if (c=='.')
{
cin>>y;
if (t[y]) cout<<'+'<<'\n';
else cout<<'-'<<'\n';
if (t[y]) t[y]=x;
out.push(mp(y,x));
}
}
return 0;
}
H.火车载客
写了n次,果然没我想的那么简单啊啊啊
贪心考虑,一定是让第 \(k\) 组所有人上车最优。若所有人都上车人数超过限制,那么对于时间 \([s,t]\) 内车上的人员,需要下车的人数就是 \(sum-c\) (\(sum\)表示当前车上人数),而为了最优,一定是使下车时间靠后的人下车,这里就可以用堆维护。
将第 \(k\) 组分成上车、下车两部分分别处理。对于第 \(id\) 次,若当前下车,那么就将答案加上该组人数,再将 \(sum\) 减去 \(id\) 的人数;若当前上车,就在限制 \(c\) 内尽量往里面塞人。需要注意在塞人前,要提前把在该站下车的人下车,不然会影响塞人。如果 \(id\) 还可以再塞人且更优,那就用第 \(id\) 次的人替换先前不优的人。
AC代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int k,n,c;
struct train
{
int s,t,p,id;//p:还有多少人可上车 id:已上车多少人
bool operator < (const train &a) const { return t<a.t; }
}a[N];
struct node
{
int tme,id,op;
bool operator < (const node &a) const
{
if (tme!=a.tme) return tme<a.tme;
else return op>a.op;
}
}b[N<<1];
int tol;
priority_queue <train> q;
int ans,sum;
signed main()
{
scanf("%lld%lld%lld",&k,&n,&c);
for (int i=1;i<=k;i++)
{
scanf("%lld%lld%lld",&a[i].s,&a[i].t,&a[i].p);
b[++tol]={a[i].s,i,0};
b[++tol]={a[i].t,i,1};
}
sort(b+1,b+1+tol);
for (int i=1;i<=tol;i++)
{
int id=b[i].id;
if (b[i].op==1)
{
ans+=a[id].id ,sum-=a[id].id;
a[id].id=0;
continue;
}
//该下下
while (!q.empty()&&q.top().t<b[i].tme) { a[q.top().id].id=0 ; q.pop(); }
//尽量上
while (sum<c&&a[id].p)
{
int add=min(c-sum,a[id].p);
a[id].id+=add,a[id].p-=add;
q.push((train){a[id].s,a[id].t,add,id});
sum+=add;
}
//更优就上
while (!q.empty()&&a[id].p&&a[id]<q.top())
{
train tmp=q.top();
q.pop();
int add=min(a[id].p,tmp.p);
tmp.p-=add;
a[tmp.id].id-=add;
a[id].id+=add,a[id].p-=add;
if (tmp.p) q.push(tmp);
q.push((train){a[id].s,a[id].t,add,id});
}
}
printf("%lld",ans);
return 0;
}
I.矩阵选数
数据存在问题