一、并查集
eg1. 《算法笔记》P332——好朋友
思路分析:
利用并查集,最终分成的组数就是集合的个数,也就是最后根节点的个数。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
int father[maxn];
bool isRoot[maxn]; // 判断结点是否为根节点
void init(int n) // 初始化,每个元素的父亲结点设为自己
{
for(int i=1; i<=n; i++)
{
father[i] = i;
isRoot[i] = true;
}
}
/*
int findFather(int x) // 无路径压缩查找
{
while(x != father[x])
{
x = father[x];
}
return x;
}
*/
int findFather(int x) // 路径压缩查找
{
int a = x;
while(x != father[x])
{
x = father[x];
}
while(a != father[a]) // 所查找的元素到根节点之间,所经过的元素的父亲结点都设为根节点
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
void Union(int a, int b) // 合并
{
int father_a = findFather(a);
int fatehr_b = findFather(b);
if(father_a != fatehr_b)
{
father[father_a] = fatehr_b;
isRoot[father_a] = false; // 合并后将其中一个根节点取消掉
}
}
int main()
{
freopen("input.txt", "r", stdin);
int n, m;
scanf("%d %d", &n, &m);
init(n);
for(int i=1; i<=m; i++)
{
int temp1, temp2;
scanf("%d %d", &temp1, &temp2);
Union(temp1, temp2);
}
int ans = 0;
for(int i=1; i<=n; i++)
{
ans += isRoot[i];
}
printf("%d", ans);
fclose(stdin);
return 0;
}
eg2. 《王道》P106——畅通工程
思路分析:
利用并查集,找出分成的组个数,需要新建的路个数为组数减一。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1010;
int father[maxn];
bool isRoot[maxn];
int ans = 0;
void init(int n)
{
ans = 0;
for(int i=1; i<=n; i++)
{
father[i] = i;
isRoot[i] = true;
}
}
int findFather(int x)
{
int a = x;
while(x != father[x])
{
x = father[x];
}
while(a != father[a])
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
void Union(int a, int b)
{
int father_a = findFather(a);
int father_b = findFather(b);
if(father_a != father_b)
{
father[father_b] = father_a;
isRoot[father_b] = false;
}
}
int main()
{
freopen("input.txt", "r", stdin);
int n, m;
while(scanf("%d %d", &n, &m)!=EOF && n!=0)
{
init(n); // 初始化
int temp1, temp2;
for(int i=0; i<m; i++)
{
scanf("%d %d", &temp1, &temp2);
Union(temp1, temp2);
}
for(int i=1; i<=n; i++)
{
ans += isRoot[i];
}
printf("%d\n", ans-1);
}
fclose(stdin);
return 0;
}
eg3. PAT A1107 Social Clusters
思路分析:
由于是根据兴趣进行分类、并查集操作,所以可以先将所有人存入其对应的兴趣列表中,再依次对每个兴趣列表进行并查集操作。
代码(AC)
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1010;
int father[maxn];
int isRoot[maxn];
vector<int> hobby[maxn];
void init(int n)
{
for(int i=1; i<=n; i++)
{
father[i] = i;
isRoot[i] = 1;
}
}
int findFather(int x)
{
int a = x;
while(x != father[x])
{
x = father[x];
}
while(a != father[a])
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
void Union(vector<int> v)
{
int father_a = findFather(v[0]);
for(int i=1; i<v.size(); i++)
{
int father_b = findFather(v[i]);
if(father_a != father_b)
{
father[father_b] = father_a;
isRoot[father_a] += isRoot[father_b];
isRoot[father_b] = 0;
}
}
}
bool cmp(int a, int b)
{
return a > b;
}
int main()
{
freopen("input.txt", "r", stdin);
int n;
scanf("%d", &n);
init(n);
for(int i=1; i<=n; i++)
{
int num, temp;
scanf("%d: ", &num);
for(int j=0; j<num; j++)
{
scanf("%d", &temp);
hobby[temp].push_back(i);
}
}
for(int i=1; i<=1000; i++)
{
if(hobby[i].size() > 1)
{
Union(hobby[i]);
}
}
int ans = 0;
for(int i=1; i<=n; i++)
{
if(isRoot[i] > 0)
{
ans++;
}
}
printf("%d\n", ans);
sort(isRoot+1, isRoot+n+1, cmp);
for(int i=1; i<=ans; i++)
{
printf("%d", isRoot[i]);
if(i < ans)
printf(" ");
}
fclose(stdin);
return 0;
}
eg4. PAT A1021 Deepest Root
题目大意:
给出N个结点与N-1条边,问是否能形成一棵树。若能从中选取根节点使树的深度最大。
思路分析:
利用并查集,若最后只有一个集合,说明可以形成树,否则不能。对于形成的树,对树进行DFS。
先任意选择一个结点,从该结点开始遍历,获取能到达的最深顶点(集合A);
然后从集合A中任一个结点开始遍历,获取能到达的最深顶点(集合B);
集合A与集合B的并集,即为所求树高最大的根节点。
注:因为存储的是无向图,所以在进行DFS过程中,要将已经走过的路径跳过去,防止出现环路。
代码(未AC)
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 100010;
vector<int> G[maxn];
int father[maxn];
bool isRoot[maxn];
void init(int n)
{
for(int i=1; i<=n; i++)
{
father[i] = i;
isRoot[i] = true;
}
}
int findFather(int x)
{
int a = x;
while(x != father[x])
{
x = father[x];
}
while(a != father[a])
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
void Union(int a, int b)
{
int father_a = father[a];
int father_b = father[b];
if(father_a != father_b)
{
father[father_b] = father_a;
isRoot[father_b] = false;
}
}
int maxH = 0; // 树的最大高度
vector<int> temp, ANS;
void DFS(int u, int height, int pre) // u为当前访问结点编号,height为当前树高(初值设1),pre为u的父节点(初值设-1)
{
if(height > maxH)
{
temp.clear();
temp.push_back(u);
maxH = height;
}
else if(height == maxH) // temp中存放遍历到的深度最大结点
{
temp.push_back(u);
}
for(int i=0; i<G[u].size(); i++)
{
if(G[u][i] == pre) // 因为是无向图,所以在树的遍历过程中,要将已经走过的路径跳过去,否则会形成环路
{
continue;
}
DFS(G[u][i], height+1, u);
}
}
int main()
{
freopen("input.txt", "r", stdin);
int n;
scanf("%d", &n);
init(n);
for(int i=0; i<n-1; i++)
{
int temp1, temp2;
scanf("%d %d", &temp1, &temp2);
G[temp1].push_back(temp2);
G[temp2].push_back(temp1);
}
for(int i=1; i<=n; i++)
{
int a = i;
for(int j=0; j<G[i].size(); j++)
{
int b = G[i][j];
Union(a, b);
}
}
int ans = 0;
for(int i=1; i<=n; i++)
{
ans += isRoot[i];
}
if(ans != 1)
{
printf("Error: %d components\n", ans);
}
else
{
DFS(1, 1, -1); // 从任一个结点开始遍历
ANS = temp;
maxH = 0;
temp.clear();
DFS(ANS[0], 1, -1); // 从任一个最深结点开始遍历
for(int i=0; i<temp.size(); i++)
{
ANS.push_back(temp[i]); // 将两组序列合并起来
}
sort(ANS.begin(), ANS.end());
printf("%d\n", ANS[0]);
for(int i=1; i<ANS.size(); i++) // 重复的不输出
{
if(ANS[i] != ANS[i-1])
{
printf("%d\n", ANS[i]);
}
}
}
fclose(stdin);
return 0;
}
二、图的遍历
eg1. PAT A1013 Battle Over Cities
题目大意:
给定一个无向图,当删除某个结点时,将会同时把与之连接的边一起删除。求解删除不同结点后,至少需要增加多少条边才能使图变为连通(所有操作均在原图上进行)。
思路分析:
本题其实就是求解连通块的个数,因而可以使用并查集,也可以使用图的DFS。本例中使用DFS方法。
注:删除结点,其实不需要真正的在数据结构中删除,而只需要在DFS过程中,遇到该已删除结点,直接return即可。
代码(AC)
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1010;
vector<int> G[maxn]; // 邻接表
bool vis[maxn] = {false}; // 记录结点是否被访问过
int ans = 0;
void DFS(int v, int del) // DFS遍历顶点v所在的连通块,忽略顶点del
{
if(v == del)
return ; //当遍历到删除结点del时,直接返回,可以起到结点删除的作用,而不需要在数据结构中真正删除
vis[v] = true; // 标记为已访问过
for(int i=0; i<G[v].size(); i++)
{
if(vis[G[v][i]] == false)
{
DFS(G[v][i], del);
}
}
}
void DFSTrave(int n, int del) // 遍历图的所有结点
{
for(int u=1; u<=n; u++)
{
if(u != del && vis[u] == false)
{
ans++; // 连通块的个数
DFS(u, del);
}
}
}
int main()
{
freopen("input.txt", "r", stdin);
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
for(int i=0; i<m; i++)
{
int temp1, temp2;
scanf("%d %d", &temp1, &temp2);
G[temp1].push_back(temp2);
G[temp2].push_back(temp1); // 邻接表存储无向图
}
for(int i=0; i<k; i++)
{
ans = 0;
fill(vis, vis+maxn, false);
int del;
scanf("%d", &del);
DFSTrave(n, del); // 遍历图中所有结点,跳过已删除结点
printf("%d\n", ans-1);
}
fclose(stdin);
return 0;
}