并查集解决集合的合并问题,每一个结点有一个结点号,同时存储着一个指向其双亲结点的号码,每个集合都有一个根结点,根结点的双亲节点号为-1,表示无双亲,为了查询一个结点对应的集合的根结点,可以对查询过程实现优化,在查询过程中,使得每个扫描过的结点不再指向双亲结点,而把整个集合的根结点作为自己的双亲结点即可,这样以后的查询合并过程可以大大简化,合并是并查集的主要目的,找到两个集合的根结点,使得一个根结点将另一个集合的根结点作为自己的双亲结点,当然此根结点所在的集合的结点指向的根结点仍为自己,但是可以在以后的查询过程中实现修改,这就是并查集的主要思想。
问题1,n个小镇,已有m条公路连接任意2个小镇,求还要修多少公路,才能使得所有小镇都可以间接或直接抵达,思想就是求可以分为多少个集合,集合之间用一条公路连接,所以最后是 根结点数-1。公路是双向的,加入集合的小镇编号都是可以直接或间接互相到达的。
代码为:
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1000
int Tree[N];
//优化只能在查询过程中路过的结点产生,但是实际上算法实现了每个集合深度最多为2
//可能会出现叶子结点的Tree[x]并不指向整个集合的根结点的情况
int findRoot(int x)
{
if(Tree[x] == -1)
return x;
else
{
int temp = findRoot(Tree[x]);
Tree[x] = temp;//整个集合的根结点,所有的一路查询过的x结点的Tree[x]都改为整个集合的根结点
return temp;
}
}
int main()
{
int m, n;
while(scanf("%d",&n) != EOF && n != 0)
{
scanf("%d", &m);
for(int i = 0; i < n; i++)
Tree[i] = -1;//未成集合前所有的都是根结点
for(int j = 0; j < m; j++)
{
int a, b;
scanf("%d%d", &a, &b);
a = findRoot(a);
b = findRoot(b);
if(a != b)
Tree[a] = b;
}
int ans = 0;
for(int i = 0; i < n; i++)
if(Tree[i] == -1)
ans++;
printf("%d\n", ans - 1);
}
return 0;
}
运行结果:
第2个问题,设好朋友是可以传递的,a与b是好朋友,b与c是好朋友,那么a与c是好朋友,实际和上面一样是一种传递关系,
给出好友关系表,找到好友数最多的集合中包含的人数,注意sum[i]的初始值是1:
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1000001
int Tree[N];
int sum[N];
//优化只能在查询过程中路过的结点产生,但是实际上算法实现了每个集合深度最多为2
//可能会出现叶子结点的Tree[x]并不指向整个集合的根结点的情况
int findRoot(int x)
{
if(Tree[x] == -1)
return x;
else
{
int temp = findRoot(Tree[x]);
Tree[x] = temp;//整个集合的根结点,所有的一路查询过的x结点的Tree[x]都改为整个集合的根结点
return temp;
}
}
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
//注意是N
fill(Tree, Tree + N, -1); //注意Tree+N是尾元素的下一位
fill(sum, sum + N, 1);//经验证fill是可行的,要注意的是fill只能用于数组
//memset(a, 0, sizeof(a))注意memset的写法
for(int i = 0; i < n; i++)
{
int a, b;
scanf("%d%d", &a, &b);
a = findRoot(a);
b = findRoot(b);
if(a != b)
{
Tree[a] = b;
sum[b] += sum[a];
}
}
int ans = 1;
for(int j = 0; j < N; j++)
if(Tree[j] == -1 && sum[j] > ans)
ans = sum[j];
printf("%d\n", ans);
}
return 0;
}
运行结果:
2. 利用并查集解决最小生成树的问题:最小生成树是无向图中包含了所有顶点,边数为顶点数-1的无向图的一部分,所有顶点在最小生成树中都是可达的,用并查集来解决就是在一个集合中,最小生成树要求所得到的树的边的权值之和应该是所有满足上面要求中的图的最小的。
kruskal算法思想是利用并查集,首先将所给的所有边按照递增的顺序进行排列,依次按照递增顺序所给的边,若所给边的两个顶点不在一个集合中,则将它们合并,并计算权值加入到最小生成树中,否则该边是无用的舍弃。一直进行下去,直到加入了n-1条边,若小于n-1则不存在最小生成树,要注意判断。
题目为给出n个村庄以及n*(n-1)/2条边以及权值,求最小的公路的总长度,代码为:
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 101
int Tree[N];
struct Edge{
int a,b;
int cost;
}edge[6000];
//递增排序
bool cmp(Edge e1, Edge e2)
{
return e1.cost < e2.cost;
}
int findRoot(int x)
{
if(Tree[x] == -1)
return x;
else
{
int temp = findRoot(Tree[x]);
Tree[x] = temp;//整个集合的根结点,所有的一路查询过的x结点的Tree[x]都改为整个集合的根结点
return temp;
}
}
int main()
{
int n;
while(scanf("%d", &n) != EOF && n != 0)
{
//编号从1开始,题目要求共n*(n-1)/2条边,即任两个地点之间的距离消去重复的计算
for(int i = 1; i <= n * (n - 1) / 2; i++)
{
scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].cost);
}
sort(edge + 1, edge + 1 + n * (n - 1) / 2, cmp);//注意写法
for(int i = 1; i <= n; i++)
Tree[i] = -1;
int ans = 0;
for(int i = 1; i <= n*(n - 1)/2; i++)
{
int v1 = edge[i].a;
int v2 = edge[i].b;
v1 = findRoot(v1);
v2 = findRoot(v2);
if(v1 != v2)
{
Tree[v1] = v2;
ans += edge[i].cost;//只可能是在合并集合时(可能合并1个单独的结点)时才可能是最小生成树的一条边
}
}
printf("%d\n", ans);
}
return 0;
}
运行结果:
最小生成树问题2,给出n个坐标,求最短的可以是n个点连通的最小生成树,一定可以连通,一定存在最小生成树,只要求出各个点坐标之间的距离,问题就和上面的问题完全一致了,n个点,size条边,代码为:
#include<cstdio>
#include<math.h>//sqrt(double x)
#include<algorithm>
using namespace std;
#define N 101
int Tree[N];
struct Edge{
int a,b;
double cost;
}edge[6000];
//递增排序
bool cmp(Edge e1, Edge e2)
{
return e1.cost < e2.cost;
}
struct Point{
double x, y;
double getDistance(Point A)
{
double tmp = (x - A.x) * (x - A.x) + (y - A.y) * (y - A.y);
return sqrt(tmp);
}
}p[101];
int findRoot(int x)
{
if(Tree[x] == -1)
return x;
else
{
int temp = findRoot(Tree[x]);
Tree[x] = temp;//整个集合的根结点,所有的一路查询过的x结点的Tree[x]都改为整个集合的根结点
return temp;
}
}
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
//编号从1开始,题目要求共n*(n-1)/2条边,即任两个地点之间的距离消去重复的计算
for(int i = 1; i <= n; i++)
{
scanf("%lf%lf", &p[i].x, &p[i].y);
}
int size = 0; //由点得到的边的总数
//得到由n个坐标之间的所有线段,去除重复的
//边的下标从1开始,坐标i与坐标j之间的距离
for(int i = 1; i <= n; i++)
for(int j = i + 1; j <= n; j++)
{
edge[size].a = i;
edge[size].b = j;
edge[size].cost = p[i].getDistance(p[j]);
size++;
}
sort(edge, edge + size, cmp);//注意写法
for(int i = 1; i <= n; i++)
Tree[i] = -1;
double ans = 0;
int num_edge = 0;
for(int i = 0; i < size; i++)
{
int v1 = edge[i].a;
int v2 = edge[i].b;
v1 = findRoot(v1);
v2 = findRoot(v2);
if(v1 != v2)
{
Tree[v1] = v2;
ans += edge[i].cost;//只可能是在合并集合时(可能合并1个单独的结点)时才可能是最小生成树的一条边
num_edge++;
if(num_edge == n - 1)
break;
}
}
//num_edge即最小生成树的边数必须是顶点总数-1才行
//当然本题实际上不需要这个判断,因为是给的坐标,边可以任意构造,一定可以构造出连通图,则一定有最小连通图
if(num_edge != n - 1)
printf("error!\n");
else
printf("%.2f\n", ans);//double 用%lf输入,用%f输出
}
return 0;
}
运行结果: