并查集(总结)
T1 逐个击破
题目:题目背景
三大战役的平津战场上,傅作义集团在以北平、天津为中心,东起唐山西至张家口的铁路线上摆起子一字长蛇阵,并企图在溃败时从海上南逃或向西逃窜。为了就地歼敌不让其逃走,*制定了先切断敌人东西两头退路然后再逐个歼灭敌人的战略方针。秉承伟大军事家的战略思想,作为一个有智慧的军长你,遇到了一个类似的战场局面。
题目描述
现在有N个城市,其中K个被敌方军团占领了,N个城市间有N-1条公路相连,破坏其中某条公路的代价是已知的,现在,告诉你K个敌方军团所在的城市,以及所有公路破坏的代价,请你算出花费最少的代价将这K个地方军团互相隔离开,以便第二步逐个击破敌人。
输入输出格式
输入格式:
第一行包含两个正整数n和k。
第二行包含k个整数,表示哪个城市别敌军占领。
接下来n-1行,每行包含三个正整数a,b,c,表示从a城市到b城市有一条公路,以及破坏的代价c。城市的编号从0开始。
输出格式:
输出一行一个整数,表示最少花费的代价。
输入输出样例
输入样例#1: 复制
5 3
1 2 4
1 0 4
1 3 8
2 1 1
2 4 3
输出样例#1: 复制
4
说明
【数据范围】
10%的数据:2≤n≤10;
100%的数据:2≤n≤100000,2≤k≤n,1≤c≤1000000。
题解:
why解释的很好,借用一下:
来发一个并查集题解
前面的几个题解其实已经挺清楚的了,我再来说一下吧。
这个题是我做的第一道有关于删边的并查集,我的第一反应是先把边建立起来,再按照要求去删掉。但一想,这样操作会很麻烦,而且说不好还会TLE OR MLE。
然后旁边的学长说,这种题应该反过来做。嗯,那就反过来做吧。
既然求的是最少需要耗费多少花费多少代价来删边,而又知道了所有边的代价,那么我们可以求建边的最大代价,再用所有的代价减去它,得出来的不就相当于删边的最小代价了吗。
而我们建边的时候要注意的是,题目要求所有敌人的据点不能连通,所以合并的时候不能让两个点在分别都属于敌人据点的情况下再合并。
需要注意的是,当一个敌人据点加入到一个并查集里面时,这个并查集里面所有的据点都会变成敌人据点。
那么,先按照道路的代价从小到大排个序,再建边就好了。
#include<iostream>
#include<algorithm>
#include<string.h>
#include<math.h>
#include<cstdio>
#include<queue>
using namespace std;
struct kkk{
int father;
bool size;//true代表被占领 false代表未被占领
}node[100010];
int n,k,now;
long long ans,all,used;
struct www{
int to,length,from;
}edge[200010];
void add(int a,int b,int c)
{
now++;
edge[now].length=c;
edge[now].from=a;
edge[now].to=b;
}//存边操作
bool cmp(www a,www b)
{
return a.length>b.length;
}
bool judge(int a,int b)
{
if ((node[a].size==true)&&(node[b].size==true)) return true;//如果两个城市都是敌人的返回true
else return false; //否则返回false
}
int getfather(int x)//找根节点,以及压缩路径
{
if (node[x].father==x) return x;
node[x].father=getfather(node[x].father);
return node[x].father;
}
void merge(int u,int v)
{
node[v].father=u;//把祖先挂上去
if (node[v].size==true) node[u].size=true;
else if (node[u].size==true) node[v].size=true;//如果这两个点有一个是敌人的城市,那么与之合并的另一个也要成为敌人的城市
}
int main()
{
int i,a,b,c,u,v;
scanf("%d%d",&n,&k);
for (i=0;i<=n;i++)
{
node[i].father=i;
node[i].size=false;
}//初始化,注意,城市是从0开始的
for (i=1;i<=k;i++)
{
scanf("%d",&a);
node[a].size=true;
}//把敌人城市标记
for (i=1;i<=n-1;i++)
{
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
all=all+c;
}//读入边,还有计算所有边的总代价
sort(edge+1,edge+n,cmp);//按边的代价从大到小排序
for (i=1;i<=n-1;i++)
{
u=getfather(edge[i].from);
v=getfather(edge[i].to);//得出边的两边的城市的并查集的根节点
if (judge(u,v)==false)//判断,如果两个点不是全都是敌人的城市,那么合并
{
merge(u,v);//合并
used=used+edge[i].length;//建边的代价增加
}
}
ans=all-used;//那么,最后的答案便是总代价减去建边的最大代价了
printf("%lld\n",ans);
return 0;
}
哦哦,对了,有一个点会卡int,所以记录答案的时候要用long long。
这应该是luogu里面最简单的一道删边的并查集了吧。
感觉此题通过相减没普遍性,下面来一个真正倒序做法
T2 星球大战
题目描述
很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治者整个星系。某一天,凭着一个偶然的机遇,一支反抗军摧毁了帝国的超级武器,并攻下了星系中几乎所有的星球。这些星球通过特殊的以太隧道互相直接或间接地连接。
但好景不长,很快帝国又重新造出了他的超级武器。凭借这超级武器的力量,帝国开始有计划地摧毁反抗军占领的星球。由于星球的不断被摧毁,两个星球之间的通讯通道也开始不可靠起来。现在,反抗军首领交给你一个任务:给出原来两个星球之间的以太隧道连通情况以及帝国打击的星球顺序,以尽量快的速度求出每一次打击之后反抗军占据的星球的连通快的个数。(如果两个星球可以通过现存的以太通道直接或间接地连通,则这两个星球在同一个连通块中)。
输入输出格式
输入格式:
输入文件第一行包含两个整数,N (1 <= N <= 2M) 和M (1 <= M <= 200,000),分别表示星球的数目和以太隧道的数目。星球用0~N-1的整数编号。
接下来的M行,每行包括两个整数X, Y,其中(0<=X<>Y
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXI=4e5+4;
int f[MAXI],head[MAXI],h[MAXI],ans[MAXI],En=0;//f为并查集,h为打击点存储的数组,ans为每次打击后的答案
bool e[MAXI]; //e来判断是否被打击掉
int find(int x)
{
if(x!=f[x]) f[x]=find(f[x]); //并查集基本函数
return f[x];
}
struct edge
{
int from;
int to; //定义一个结构体来存储邻接表
int next;
}a[MAXI];
void insert(int u,int v)
{ //邻接表存储数据
a[En].from=u;
a[En].next=head[u];
a[En].to=v;
head[u]=En;
En++;
}
int main()
{
int n,m,k,x,y,tot,i,u;
cin>>n>>m;
for(i=0;i<n;++i)
{
f[i]=i;
head[i]=-1;
}
for(i=0;i<m;++i)
{
cin>>x>>y;
insert(x,y);insert(y,x); //双向存储数据
}
cin>>k;
tot=n-k; //打击k次后所剩下的点
for(i=1;i<=k;i++)
{
cin>>x;
e[x]=true; //被打击掉后就true,并把打击的点存储到h中
h[i]=x;
}
for(i=0;i<2*m;i++)
{
if(e[a[i].from]==false&&e[a[i].to]==false) //如果都没有被打击
{
if(find(a[i].from)!=find(a[i].to)) //且之前没有连通
{
tot--; //合并这两个点并在总数减去一个
f[find(a[i].from)]=f[find(a[i].to)];//记住一定要更新父亲节点
}
}
}
ans[k+1]=tot; //这时为打击k次之后所剩下的连通块
for(int t=k;t>=1;t--) //从后往前“修复”
{
u=h[t];
tot++; //因为“修复”这个点所以多了一个点,现在总数加 1
e[u]=false; //false表示这个点没有被打击
for(i=head[u];i!=-1;i=a[i].next) //邻接表遍历它所连着的点
{
if(e[a[i].to]==false&&f[find(u)]!=f[find(a[i].to)]) //如果被连通的点没有被打击并且之前没有连通
{
tot--; //合并
f[find(a[i].to)]=f[find(u)]; //注意尽量不要到过来赋值,这样会不断改变father
}
}
ans[t]=tot; //每“修复”一个点后的有的连通块
}
for(i=1;i<=k+1;++i) cout<<ans[i]<<endl;
return 0;
}
T3 食物链
题目描述
动物王国中有三类动物 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 句话有的是真
的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
• 当前的话与前面的某些真的话冲突,就是假话
• 当前的话中 X 或 Y 比 N 大,就是假话
• 当前的话表示 X 吃 X,就是假话
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入输出格式
输入格式:
从 eat.in 中输入数据
第一行两个整数,N,K,表示有 N 个动物,K 句话。
第二行开始每行一句话(按照题目要求,见样例)
输出格式:
输出到 eat.out 中
一行,一个整数,表示假话的总数。
输入输出样例
输入样例#1: 复制
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例#1: 复制
3
说明
1 ≤ N ≤ 5 ∗ 10^4
1 ≤ K ≤ 10^5
题解:
这题%3很有意思,因为是三种动物循环,%3判断关系,并查集合并
//带权并查集 详细的看代码里的注释
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
int fa[50005],ran[50005]; //ran指种族
int n,k;
int get_fa(int x)
{
if(fa[x]==x) return x;
int f=fa[x];
fa[x]=get_fa(fa[x]); //路径压缩
ran[x]=(ran[x]+ran[f])%3;
return fa[x];
}
bool judge(int c,int x,int y)
{
int a=get_fa(x),b=get_fa(y);
//cout<<a<<" "<<ran[x]<<" "<<b<<" "<<ran[y]<<endl;
if(a==b) //说明已经过确定关系了
{
if(c==1)
{
if(ran[x]!=ran[y]) return false;
}
if(c==2)
{
if(ran[x]==1&&ran[y]!=0) return false;
if(ran[x]==2&&ran[y]!=1) return false;
if(ran[x]==0&&ran[y]!=2) return false;
}
return true;
}
else{ //说明没有确定过关系
fa[a]=b;
//cout<<c<<" "<<ran[x]<<" "<<ran[y]<<endl;
if(c==2) ran[a]=(ran[y]-ran[x]+3+1)%3;
if(c==1) ran[a]=(ran[y]-ran[x]+3)%3;
//cout<<fa[a]<<ran[a]<<endl;
a=get_fa(x); b=get_fa(y);
//cout<<"changed:"<<a<<" "<<ran[x]<<" "<<b<<" "<<ran[y]<<endl;
return true;
}
}
int main()
{
cin>>n>>k;
int ans=0;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=k;i++)
{
int c,a,b;
scanf("%d %d %d",&c,&a,&b);
if(a>n||b>n) ans++;
else if(c==2 && a==b) ans++;
else if(judge(c,a,b)) continue;
else ans++;
}
printf("%d\n",ans);
return 0;
}
做完这几个题收获很大,noip2017加油!!