优先队列,并查集

优先队列

原理是用二叉树实现,这个二叉树始终满足父亲节点大于(小于)它的两个儿子。

比如大根堆,当插入元素时,先给他插到二叉树最下面,然后慢慢与父亲节点比较然后往上移,

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;
	}
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值