0.并查集
什么是并查集?
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
常见操作
- 初始化(init)
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
void init()
{
for(int i=1;i<=n;i++)
father[i]=i;
}
- 查找(getfather)
查找元素所在的集合,即根节点。
int getfather(int x)
{
if(father[x]==x)return x;
else return father[x]=getfather(father[x]);
}
这里采用了路径压缩的方法,把i所属的集改成根结点,那么下次再搜的时候,就能在O(1)的时间内得到结果。
- 合并(merge)
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
void merge(int a,int b)
{
int f1=getfather(a);
int f2=getfather(b);
if(f1!=f2)
{
father[f1]=f2;//也可以father[f2]=f1;
其他操作;
}
}
- 带权并查集
在查找(getfather)的基础上进行优化:
int getfather(int x)
{
if(father[x]==x)return x;
else
{
int t=father[x];//记录父节点
father[x]=getfather(father[x]);
dis[x]+=dis[t];//权值更新为x到根节点的权值
return father[x];
}
}
这里的dis数组表示各点到其祖先的距离。
1.最小生成树
线路规划
时间限制: 1 Sec 内存限制: 128 MB
题目描述
有n 个村庄之间需要架设通信线路,使得任意两个村庄之间均可通信。两个村庄a, b 间可通信,当且仅当它们之间存在一条通信线路或者存在村庄c 使得a,c 和b,c 间均可通信。给出村庄之间架设通信线路的代价,求出最小的总代价。
输入
第一行包含两个整数n,m,分别表示村庄数量和可以架设通信线路的村庄对数。以下m 行,每行三个整数a,b,c,表示村庄a,b之间架设线路的代价为c(村庄从0 开始编号)。
输出
一个整数,最小总代价。
样例输入 Copy
3 3
0 1 1
1 2 1
2 0 3
样例输出 Copy
2
提示
对于50% 的数据,n<=100,m <=n^2
对于全部数据,1<=n<=10^ 5; n-1<=m<=10^ 5,所有代价均在[0, 10^6] 范围内,保证问题有解。
最小生成树kruskal算法+并查集模板题,容易WA的点既不是保证起点坐标小于终点坐标,也不是选择father[f1]=f2
还是father[f2]=f1
,更不是读入村庄对数时从1到m还是从0到m-1,而是最后的总代价可能是long long型。一开始我误看成了所有的总代价在[0,10^6]范围内,只能通过91%的数据。。。
#include<cstdio>
#include<cstdlib>
using namespace std;
typedef struct village
{
int Start,End,Weight;
}v;
int father[100005];
v vil[100005];
int getfather(int x)
{
if(father[x]==x)return x;
else return father[x]=getfather(father[x]);
}
int cmp(const void*a,const void*b)
{
v*x=(v*)a;
v*y=(v*)b;
return x->Weight-y->Weight;
}
int main()
{
int n,m,edge=0,a,b,c;
long long int cost=0;
scanf("%d %d",&n,&m);
for(int i=0;i<=n-1;i++)father[i]=i;
for(int i=0;i<=m-1;i++)
{
scanf("%d %d %d",&a,&b,&c);
vil[i].Start=a;
vil[i].End=b;
vil[i].Weight=c;
}
int f1,f2;
qsort(vil,m,sizeof(vil[0]),cmp);
for(int i=0;i<=m-1;i++)
{
f1=getfather(vil[i].Start);
f2=getfather(vil[i].End);
if(f1!=f2)
{
father[f1]=f2;
edge++;
cost+=vil[i].Weight;
if(edge==n-1)break;
}
}
printf("%lld",cost);
return 0;
}
/**************************************************************
Language: C++
Result: 正确
Time:65 ms
Memory:3852 kb
****************************************************************/
2.最大生成树
sunflower
时间限制: 1 Sec 内存限制: 128 MB
题目描述
小 N 经常去小 T 家的花园里散步,小 T 家的花园有 N 个长的一样的亭子和 M 条道路连接着亭子,但是小 T 的花园太过于乱了,小 N 作为一个路痴经常进去了之后找不到出来的路,一直在环里面绕圈。于是小 N 要让小 T 把其中的某些路种上向日葵,使得剩下的路不会出现环。因为向日葵不方便种,而第i条路长Li,需要Li个向日葵去种,于是小 T 想知道他最少要种多少向日葵才能满足小 N 的要求呢?
输入
第一行两个整数N,M,表示花园的亭子数目和道路数目。
接下来M行,每行3个整数A,B,C,表示有一条连接着A和B的长度为C的道路。
输出
一行一个整数,表示小 T 最少需要种的向日葵数目。
样例输入 Copy
5 5
1 2 5
1 4 4
3 4 3
2 3 2
3 5 1
样例输出 Copy
2
提示
对于100%的数据,1≤N≤10 ^5,1≤M≤2×10 ^5,0<C≤10 ^6
类比“破圈法”的原理,问题等价于在不改变连通性的前提下每次删除权值最小的边,直到不能删除为止,即最大生成树问题。只需把kruskal算法的排序方式修改为按权值从大到小排序。
#include<cstdio>
#include<cstdlib>
using namespace std;
typedef struct Edge
{
int Start,End,cost;
}edge;
int cmp(const void*a,const void*b)
{
edge*x=(edge*)a;
edge*y=(edge*)b;
return y->cost-x->cost;
}
edge road[200005];
int father[100005];
int n,m,a,b,c,cnt=0;
int getfather(int v)
{
if(father[v]==v)return v;
else return father[v]=getfather(father[v]);
}
int main()
{
long long int sum=0;
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d %d %d",&a,&b,&c);
road[i].Start=a;
road[i].End=b;
road[i].cost=c;
sum+=road[i].cost;
}
qsort(road+1,m,sizeof(road[1]),cmp);//按边的权值从大到小排序
for(int i=1;i<=n;i++)father[i]=i;
long long int sum1=0;
int fat1,fat2;
for(int i=1;i<=m;i++)
{
fat1=getfather(road[i].Start);
fat2=getfather(road[i].End);
if(fat1!=fat2)
{
sum1+=road[i].cost;
father[fat2]=fat1;
cnt++;
if(cnt==n-1)break;
}
}
printf("%lld",sum-sum1);
return 0;
}
/**************************************************************
Language: C++
Result: 正确
Time:128 ms
Memory:6196 kb
****************************************************************/
3.连通图问题
tunnel
时间限制: 1 Sec 内存限制: 128 MB
题目描述
一座小镇正在着手建造自己的地铁线路网。
小镇坐落在许多小岛上,小岛之间通过隧道或者桥梁连接。地铁就在这些已有的桥梁和隧道的基础上建成。
由于地铁主要是在地下,所以桥梁用得越少越好。小镇的居民希望能够仅通过地铁就能在任意两座小岛之间往返旅行。
幸运的是,我们有足够多的隧道和桥梁来满足这个要求,所以不需要建造新的隧道或桥梁。
你的任务是计算需要构建这样一个地铁线路网最少需要用到几座桥梁。
输入
输入包括K+M+1行。
第一行包括三个数N,M,K,其中N为小岛数量,M为隧道数量,K为桥梁数量。
以下M行每行有两个数,表示相应的隧道连接的小岛的编号,编号从1到N。
再往下K行每行用同样的方式描述一座桥梁。
输出
输出一个数,即最少需要用到的桥梁数。
样例输入 Copy
6 3 4
1 2
2 3
4 5
1 3
3 4
4 6
5 6
样例输出 Copy
2
提示
对于100%的数据,1≤N≤10000,1≤K≤12000,1≤M≤12000
由黑体部分知,最终得到的图一定是连通图,所以只需要对隧道进行并查集,得到若干连通分支,统计连通分支数(每个点的祖先节点为当前节点)-1就得到答案。
#include<cstdio>
typedef struct node
{
int Start,End;
}edge;
edge tunnel[12005];
edge bridge[12005];
int father[10005];
int k,n,m;
int getfather(int x)
{
if(father[x]==x)return x;
else return father[x]=getfather(father[x]);
}
int main()
{
scanf("%d %d %d",&n,&m,&k);
int cnt=0;
for(int i=1;i<=m;i++)
{
scanf("%d %d",&tunnel[i].Start,&tunnel[i].End);
}
for(int i=1;i<=k;i++)
{
scanf("%d %d",&bridge[i].Start,&bridge[i].End);
}
for(int i=1;i<=n;i++)father[i]=i;
int f1,f2;
for(int i=1;i<=m;i++)
{
f1=getfather(tunnel[i].Start);
f2=getfather(tunnel[i].End);
if(f1!=f2)father[f2]=f1;
}
for(int i=1;i<=n;i++)
if(father[i]==i)cnt++;
printf("%d",cnt-1);
return 0;
}
/**************************************************************
Language: C++
Result: 正确
Time:11 ms
Memory:1344 kb
****************************************************************/
4.判断有无环
无线广播
时间限制: 2 Sec 内存限制: 256 MB
某广播公司要在一个地区架设无线广播发射装置。该地区共有n个小镇,每个小镇都要安装一台发射机并播放各自的节目。
不过,该公司只获得了FM104.2和FM98.6两个波段的授权,而使用同一波段的发射机会互相干扰。已知每台发射机的信号覆盖范围是以它为圆心,20km为半径的圆形区域,因此,如果距离小于20km的两个小镇使用同样的波段,那么它们就会由于波段干扰而无法正常收听节目。现在给出这些距离小于20km的小镇列表,试判断该公司能否使得整个地区的居民正常听到广播节目。
输入
第一行为两个整数n,m,分别为小镇的个数以及接下来小于20km的小镇对的数目。 接下来的m行,每行2个整数,表示两个小镇的距离小于20km(编号从1开始)。
输出
如果能够满足要求,输出1,否则输出-1。
样例输入 Copy
4 3
1 2
1 3
2 4
样例输出 Copy
1
限制
1 ≤ n ≤ 10000
1 ≤ m ≤ 30000
不需要考虑给定的20km小镇列表的空间特性,比如是否满足三角不等式,是否利用传递性可以推出更多的信息等等。
将小于20km看成连通关系,由此可得所有给定的点可以形成图,易验证当图中不存在环或存在长度为偶数的环时能够满足要求,其余情况下不满足要求,因此问题转换成寻找是否存在环和环的长度问题。
判断回路中是否存在环可以采用并查集,当两个点(起点、终点)的祖先结点相同时,则就可以构成一个环。
利用dis数组记录结点到祖先的距离,对于一条边上的两个端点,通过找规律可以发现当两个端点到祖先的距离之差为偶数时,加上当前的边,恰好得到环的长度为奇数;反之,当两个端点到祖先的距离之差为奇数时,加上当前的边,恰好得到环的长度为偶数。
#include<cstdio>
using namespace std;
int father[10005];
typedef struct Node
{
int Start,End;
}node;
node town[30005];
int dis[10005]={0};//表示各点到其祖先的距离
bool cir=false;//标记是否有环
bool flag=false;//标记环的长度是否为奇数
//并查集
int getfather(int x)//找祖先函数
{
if(father[x]==x)return x;
else
{
int t=father[x];
father[x]=getfather(father[x]);
dis[x]+=dis[t];//更新结点到祖先的距离
return father[x];
}
}
int main()
{
int n,m,a,b;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)father[i]=i;//初始化,将每个点的祖先设为自己
for(int i=1;i<=m;i++)
{
scanf("%d %d",&a,&b);
if(a>b)
{
int t=a;
a=b;
b=t;
}
town[i].Start=a;
town[i].End=b;
}
int f1,f2;
for(int i=1;i<=m;i++)
{
f1=getfather(town[i].Start);
f2=getfather(town[i].End);
if(f1==f2)//祖先相同,说明形成环
{
cir=true;
if((dis[town[i].Start]-dis[town[i].End])%2==0)flag=true;//环的长度为奇数
}
else//祖先不同
{
father[f1]=f2;//使祖先相同
dis[f1]=dis[town[i].End]+1-dis[town[i].Start];//更新距离
}
}
if(cir==false)printf("1");//没有形成环
else if(flag==false)printf("1");//形成环且环的长度为偶数
else printf("-1");//形成环且环的长度为奇数
return 0;
}
/**************************************************************
Language: C++
Result: 正确
Time:4 ms
Memory:4816 kb
****************************************************************/