优先队列
原理是用二叉树实现,这个二叉树始终满足父亲节点大于(小于)它的两个儿子。
比如大根堆,当插入元素时,先给他插到二叉树最下面,然后慢慢与父亲节点比较然后往上移,
pop操作是先将最后面的元素代替根节点,然后慢慢与两个儿子比较,与较大(小)儿子交换,往下移。
下面是合并果子的手撸优先队列版本:
#include<bits/stdc++.h>
using namespace std;
int a[100010];
int cnt=0;
int n;
void push(int x)
{
a[++cnt]=x;
int pos=cnt;
while(pos/2>=1&&a[pos]<a[pos/2])
{
swap(a[pos],a[pos/2]);
pos/=2;
}
}
int top()
{
return a[1];
}
void pop()
{
a[1]=a[cnt];
cnt--;
int f=1,s=f*2;
if(s+1<=cnt&&a[s+1]<a[s]) s=s+1;
while(s<=cnt&&a[f]>a[s])
{
swap(a[f],a[s]);
f=s;
s=f*2;
if(s+1<=cnt&&a[s+1]<a[s]) s=s+1;
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
push(x);
}
long long ans=0;
for(int i=1;i<n;i++)
{
int k1=top();
pop();
int k2=top();
pop();
ans+=k1+k2;
push(k1+k2);
}
cout<<ans;
}
STL写法
大根堆
priority_queue<int> q;
小根堆
priority_queue<int,vector<int>,greater<int> > q;
例题:
Running median
1002-Running Median_2021秋季算法入门班第五章习题:优先队列、并查集 (nowcoder.com)
给定一个数组,按个数依次为1,3,5...求中位数。
思路:一开始中位数是数组第一个元素,用一个小根堆来维护中位数及比它大的数,大根堆维护比中位数小的数,然后保证小根堆始终比大根堆元素多且多不超过一个(这一处是细节),每次询问中位数的时候就是小根堆的堆顶元素。
#include<bits/stdc++.h>
using namespace std;
priority_queue<int> q1;
priority_queue<int,vector<int>,greater<int> > q2;
int a[10000];
vector<int> ans;
int main()
{
int t;
cin>>t;
for(int tt=1;tt<=t;tt++)
{
while(!q1.empty()) q1.pop();
while(!q2.empty()) q2.pop();
ans.clear();
int k,n;
cin>>k>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int cnt=0;
for(int i=1;i<=n;i++)
{
if(q2.empty())
{
q2.push(a[i]);
ans.push_back(a[i]);
cnt++;
continue;
}
if(a[i]>q2.top()) q2.push(a[i]);
else q1.push(a[i]);
while(q1.size()>q2.size())//两个while循环保证两个堆之间的元素数量关系
{
q2.push(q1.top());
q1.pop();
}
while(q2.size()>q1.size()+1)
{
q1.push(q2.top());
q2.pop();
}
if(i%2==1)
{
ans.push_back(q2.top());
}
}
cout<<k<<' '<<n/2+n%2<<endl;
for(int i=0;i<ans.size();i++)
{
if(i > 0 && i % 10 == 0) putchar('\n');
if(i % 10) putchar(' ');
cout<<ans[i];
}
cout<<endl;
}
}
这种用到两个堆的,一个小根堆一个大根堆,我们关注的是两个堆之间的东西,并且保证两个堆数量差不大于一,这种模型叫做堆顶堆
例题(挺难的)
在计算机中,CPU只能和高速缓存Cache直接交换数据。当所需的内存单元不在Cache中时,则需要从主存里把数据调入Cache。此时,如果Cache容量已满,则必须先从中删除一个。
例如,当前Cache容量为3,且已经有编号为10和20的主存单元。 此时,CPU访问编号为10的主存单元,Cache命中。 接着,CPU访问编号为21的主存单元,那么只需将该主存单元移入Cache中,造成一次缺失(Cache Miss)。 接着,CPU访问编号为31的主存单元,则必须从Cache中换出一块,才能将编号为31的主存单元移入Cache,假设我们移出了编号为10的主存单元。 接着,CPU再次访问编号为10的主存单元,则又引起了一次缺失。
我们看到,如果在上一次删除时,删除其他的单元,则可以避免本次访问的缺失。 在现代计算机中,往往采用LRU(最近最少使用)的算法来进行Cache调度——可是,从上一个例子就能看出,这并不是最优的算法。
对于一个固定容量的空Cache和连续的若干主存访问请求,聪聪想知道如何在每次Cache缺失时换出正确的主存单元,以达到最少的Cache缺失次数。
1006-[JSOI2010]缓存交换_2021秋季算法入门班第五章习题:优先队列、并查集 (nowcoder.com)
思路:
贪心策略:下一次越晚出现的越早退出Cache缓存区
用一个大根堆来维护每个位置的下一次访问时间,每次需要删掉单元时删掉下一次出现最晚的单元,这里面需要很多技巧(map,nex数组,vis数组),具体看代码:
#include<bits/stdc++.h>
using namespace std;
map<int,int> pos;
priority_queue<int> q;
int vis[100010];
int nex[100020];
int a[100010];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=n;i>=1;i--)
{
if(!pos.count(a[i])) nex[i]=100010;
else nex[i]=pos[a[i]];
pos[a[i]]=i;
}
int ans=0,cnt=0;
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
ans++;
if(cnt<m)
{
cnt++;
vis[nex[i]]=1;
q.push(nex[i]);
}else{
vis[q.top()]=0;
q.pop();
q.push(nex[i]);
vis[nex[i]]=1;
}
}else{
vis[nex[i]]=1;
q.push(nex[i]);//这一步很重要,即使当前不需要pop掉单元,也要让它的下一次的vis
//变为1,然后他的nex也要push到q里面以取代原先的nex。
}
}
cout<<ans;
}
例题:
在一个游戏中,tokitsukaze需要在n个士兵中选出一些士兵组成一个团去打副本。
第i个士兵的战力为v[i],团的战力是团内所有士兵的战力之和。
但是这些士兵有特殊的要求:如果选了第i个士兵,这个士兵希望团的人数不超过s[i]。(如果不选第i个士兵,就没有这个限制。)
tokitsukaze想知道,团的战力最大为多少。1004-tokitsukaze and Soldier_2021秋季算法入门班第五章习题:优先队列、并查集 (nowcoder.com)
思路:
考虑枚举每一个s[i],将s[i]从大到小排序,然后枚举每一位士兵,假设某时刻可以入选的士兵名额为x,则这x个士兵一定是在s大于等于x的士兵中去选它的前x个士兵,这些士兵可以用一个小根堆来维护,每当人数超过限制就踢出去战力最小的士兵,然后用tmp来记录实时的士兵总战力,由于不一定是人越少士兵总战力越大,所以要有个ans来记录最终答案,当tmp大于ans时更新ans。
代码:
#include<bits/stdc++.h>
using namespace std;
struct node{
int s,v;
}a[100010];
priority_queue<int,vector<int>,greater<int> > q;
bool cmp(node a,node b)
{
return a.s>b.s;
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].v>>a[i].s;
}
sort(a+1,a+1+n,cmp);
long long ans=0,tmp=0;
int limit;
for(int i=1;i<=n;i++)
{
limit=a[i].s;
if(q.size()<limit)
{
q.push(a[i].v);
tmp+=a[i].v;
}
else{
q.push(a[i].v);
tmp+=a[i].v;
while(q.size()>limit)
{
tmp-=q.top();
q.pop();
}
}
ans=max(ans,tmp);
}
cout<<ans;
}
变式题:
小刚在玩JSOI提供的一个称之为“建筑抢修”的电脑游戏:经过了一场激烈的战斗,T部落消灭了所有z部落的入侵者。但是T部落的基地里已经有N个建筑设施受到了严重的损伤,如果不尽快修复的话,这些建筑设施将会完全 毁坏。
现在的情况是:T部落基地里只有一个修理工人,虽然他能瞬间到达任何一个建筑,但是修复每个建筑都需要一定的时间。同时,修理工人修理完一个建筑才能修理下一个建筑,不能同时修理多个建筑。
如果某个建筑在一段时间之内没有完全修理完毕,这个建筑就报废了。你的任务是帮小刚合理的制订一个修理顺序,以抢修尽可能多的建筑。
1005-[JSOI2007]建筑抢修_2021秋季算法入门班第五章习题:优先队列、并查集 (nowcoder.com)
思路:
这道题和上面的题类型一样,都是容器容量在变化,像这种题我们一般把限制从大到小排序,这样选择就越来越多,并且保证前面的决策是可行的,然而这种题解法的关键就是可以“反悔”,在某一限制下,突然不想要前面选择的某个人(这个人往往是消耗最大的),就让他从决策容器中踢出,引入新人。
这道题就是把建筑按deadline从小到大排序,然后用一个time来记录变化的deadline,use来记录决策中花费的总时长,ans来记录当前修的建筑总量,易知ans只增不减。当总时长小于time时,有可以修的建筑就让他进入堆,这个堆是一个大根堆,然后反悔的时候就提出堆顶元素,use也对应减去它的花费,然后引入新元素,use对应加上它的花费。
#include<bits/stdc++.h>
using namespace std;
struct node{
int deadl,cost;
}a[150010];
bool cmp(node a,node b)
{
return a.deadl<b.deadl;
}
priority_queue<int> q;
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].cost>>a[i].deadl;
sort(a+1,a+1+n,cmp);
int ans=0;
int time;
int use=0;
for(int i=1;i<=n;i++)
{
time=a[i].deadl;
if(use+a[i].cost<=time)
{
use+=a[i].cost;
q.push(a[i].cost);
ans++;
}else{
if(a[i].cost<q.top())
{
use-=q.top();
q.pop();
q.push(a[i].cost);
use+=a[i].cost;
}
}
}
cout<<ans;
}
map/set的应用
set例题:
思路:
遇到这种动态插入的题,先考虑静态情况:就是现在有n个人,将这n个人放在一个xy坐标系上,易发现当一个人左下角没人的时候他有优势,所以对于动态而言,每插入一个人,先判断他是否有优势,有优势把他放入set里面,这里面set的元素是结构体node(人),并且排序是按x从小到大排,y从小到大排。
对于一个人,如何判断他是否有优势?
set里面lower_bound找到他右边第一个元素或者与他重合的元素,如果这个元素是begin或者这个元素的左边元素的y大于等于y,那么就是有优势,就把他加入set里,然后遍历它右边第一个元素,如果y比当前点y值大或等于就踢出set。每次输出set里的元素个数即可。
#include<bits/stdc++.h>
using namespace std;
struct node{
int x,y;
bool operator <(const node &a)const
{
return x<a.x||(x==a.x&&y<a.y);
}
};
multiset<node> st;
int main()
{
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
st.clear();
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
node tmp;
tmp.x=x;
tmp.y=y;
multiset<node>::iterator it=st.lower_bound(tmp);//找到右边或重合的第一个数
if(it==st.begin()||((--it)->y>y))//有优势
{
st.insert(tmp);
it=st.upper_bound(tmp);
while(it!=st.end()&&it->y>=y)
{
st.erase(it++);
}
}
cout<<st.size()<<'\n';
}
}
}
2014上海区域赛铜牌题:
思路:
贪心:
排序:由于要保证能消灭敌人,所以先将敌人按防御力排序然后再选择军队与之决斗,这样能避免由于乱选导致后面的敌人无法消灭;将我方士兵按攻击力排序。
遍历敌方士兵时,将我方可攻打该敌军的士兵加入set中,随着敌军防御力降低,我军可选择的士兵是越来越多的,然后每次从set中用二分选择防御力尽可能大于等于地方攻击力的,这样可以减少牺牲,如果找不到就直接派防御力最低的士兵上,其他防御力高点的可能在后面可以幸存。
#include<bits/stdc++.h>
using namespace std;
struct node1{
int ak,def;
bool operator < (node1 &r)const
{
return ak>r.ak;
}
}a[100010];
struct node2{
int ak,def;
bool operator <(node2 &r)const
{
return def>r.def;
}
}b[100010];
multiset<int> st;
int main()
{
int t;
cin>>t;
for(int tt=1;tt<=t;tt++)
{
int n,m;
cin>>n>>m;
st.clear();
for(int i=1;i<=n;i++) cin>>a[i].ak>>a[i].def;
for(int i=1;i<=m;i++) cin>>b[i].ak>>b[i].def;
sort(b+1,b+1+m);
sort(a+1,a+1+n);
int j=1;
int ans=n;
for(int i=1;i<=m;i++)
{
int ak=b[i].ak;
int def=b[i].def;
while(j<=n&&a[j].ak>=def)
{
st.insert(a[j].def);
j++;
}
if(st.empty())
{
ans=-1;
break;
}
multiset<int>::iterator it=st.lower_bound(ak);
if(it==st.end())
{
it=st.begin();
ans--;
}
st.erase(it);
}
cout<<"Case #"<<tt<<':'<<ans<<endl;
}
}
并查集
两种优化:
1.按秩合并
2.压缩路径
带权并查集
例题1;
思路:
由于他要查x个箱子下面的箱子数目,并且涉及集合合并操作,所以这道题用并查集。
权值就是每个箱子下面的箱子数目,用under数组存,然后再用一个num数组来存每一个栈有多少个箱子,然后这个num数组的下标是每个栈的栈底元素,所以自然的,并查集的根节点就是栈底元素,把x所在栈放在y所在栈的栈顶实际上就是对两个集合合并,x所在的栈底的under加上y所在栈底的num,y所在栈底的num加上x所在栈的num。
对于查询操作,我们在find函数里进行路径压缩,并且把查询的点的under顺便在递归的过程中算出来。
#include<bits/stdc++.h>
using namespace std;
int fa[30010];
int under[30010];
int num[30010];
int find(int x)
{
if(fa[x]==x) return x;
int f=find(fa[x]);
under[x]+=under[fa[x]];
fa[x]=f;
return f;
}
void move(int x,int y)//把x放到y上
{
int fax=find(x);
int fay=find(y);
fa[fax]=fay;
under[fax]+=num[fay];
num[fay]+=num[fax];
}
int calc(int x)
{
int fax=find(x);
return under[x];
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=30000;i++)
{
fa[i]=i;
under[i]=0;
num[i]=1;
}
for(int i=1;i<=n;i++)
{
char q;
cin>>q;
if(q=='M')
{
int xx,yy;
cin>>xx>>yy;
move(xx,yy);
}else{
int x;
cin>>x;
cout<<calc(x)<<endl;
}
}
}
扩张域并查集
食物链
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是“1 X Y”,表示X和Y是同类。
第二种说法是“2 X Y”,表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1≤N≤50,000)和K句话(0≤K≤100,000),输出假话的总数。
思路:
每个动物都可能是A,B,C
把已经被确认下来的动物(已经被说过的)的三种情况放到三个集合里(这里的集合就是并查集),比如X吃Y,那么就把X是A,Y是B放到一个集合,X是B,Y是C放到一个集合,X是C,Y是A放到一个集合,然后比如X和Y同类,那么就把两个都是A,都是B,都是C放到一个集合。
真假性判断:
如果说X和Y是同类,但是X是A,Y是B在一个集合,或者X是A,Y是C在一个集合,那么说明这是假话,X是B,C无需讨论,性质一样
如果说X吃Y,但是X是A,Y也是A,或者X是A,Y是C,说明这也是假话。
这道题很难想到用并查集就是它思路上把X是A,X是B,X是C作为并查集中的元素,然后根为第一个前提。
#include<bits/stdc++.h>
using namespace std;
int fa[200010];
int find(int x)//查找这句话的第一个前提
{
return x==fa[x]? x:fa[x]=find(fa[x]); //这里用路径压缩没影响
}
void merge(int x,int y)//把两句话合并在一个集合里,说明两句话可以同时成立
{
fa[find(x)]=find(y);
}
int main()
{
int n,k;
int cnt=0;
cin>>n>>k;
for(int i=1;i<=3*n;i++) fa[i]=i;
/*这里的i表示
i表示i是A
i+n表示i是B
i+2*n表示i是C*/
for(int i=1;i<=k;i++)
{
int op,x,y;
cin>>op>>x>>y;
if(x>n||y>n)
{
cnt++;
continue;
}
if(op==1)
{
if(find(x)==find(y+n)||find(x)==find(y+2*n))
{
cnt++;
}else{
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}
}else{
if(find(x)==find(y)||find(x)==find(y+2*n))
{
cnt++;
}else{
merge(x,y+n);
merge(x+n,y+2*n);
merge(x+2*n,y);
}
}
}
cout<<cnt;
}
扩展域并查集例题2:
思路:
区间转化为端点,不然没法用并查集
会发现一个性质:
当区间a,b为奇数时,a-1的前缀和与b的前缀和有不同奇偶性个1
当区间a,b为偶数时,a-1的前缀和与b的前缀和有相同奇偶性个1
所以还是把每个端点分成两种情况,x的前缀和为奇数或偶数
这道题还要用map去离散化
P5937 [CEOI1999] Parity Game - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<bits/stdc++.h>
using namespace std;
map<long,long>mp;
int fa[10010];
int find(int x)
{
return x==fa[x]? x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
fa[find(x)]=find(y);
}
int main()
{
int n,m;
int cnt=0;
cin>>n>>m;
if(m==0)
{
cout<<0;
return 0;
}
for(int i=1;i<=2*m;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
long long x,y;
string op;
cin>>x>>y>>op;
x--;
if(!mp.count(x))
{
mp[x]=++cnt;
mp[x+n+1]=cnt+m;
fa[cnt]=cnt;
fa[cnt+m]=cnt+m;
}
if(!mp.count(y))
{
mp[y]=++cnt;
mp[y+n+1]=cnt+m;
fa[cnt]=cnt;
fa[cnt+m]=cnt+m;
}
if(op[0]=='o')
{
if(find(mp[x])==find(mp[y])||find(mp[x+n+1])==find(mp[y+n+1]))
{
cout<<i-1;
return 0;
}else{
merge(mp[x],mp[y+n+1]);
merge(mp[x+n+1],mp[y]);
}
}else{
if(find(mp[x])==find(mp[y+n+1])||find(mp[x+n+1])==find(mp[y]))
{
cout<<i-1;
return 0;
}else{
merge(mp[x],mp[y]);
merge(mp[x+n+1],mp[y+n+1]);
}
}
if(i==m) cout<<m;
}
}