算法基础——图论(一)
目录:
- 基础知识
- 邻接矩阵
- 邻接表
- 并查集
- 最小生成树
- 应用实例
- 畅通工程【浙江大学】
- 连通图【吉林大学】
- Is It A Tree?【北京大学】
- 找出直系亲属【浙江大学】
- 统计图的连通分支数【上海交通大学】
- Head of a Gang【浙江大学】
- 还是畅通工程【浙江大学】
- 继续畅通工程【浙江大学】
- Freckles【北京大学】
- Jungle Roads【北京大学】
一、基础知识
1、邻接矩阵(adjacency matrix)
- 定义:用一个二维矩阵来表示图的信息,二维矩阵中的每个单元表述一对顶点之间的邻接关系。
- 带权图:matrix[u][v]的值来表示u和v之间边的权值,对不存在的边一般取值为∞。
- 无权图:matrix[u][v]为1或0来表示u和v之间是否有边。
- 缺点:遍历与某顶点相邻的所有顶点,则需要依次遍历二维数组中某行的所有元素,通过其值判断是否相邻。
- 适用场景:稠密图,且需要频繁判断某特定顶点对是否相邻时。
2、邻接表(adjacency list)
- 定义:为图中的每个顶点建立一个单链表,单链表中保存与该顶点相邻的所有顶点及其相关信息。
- 缺点:当邻接表需要判断顶点u和v之间是否存在关系时,需要遍历u和v的所有邻接顶点,才能判定它们之间是否存在关系。
- 适用场景:存在大量遍历邻接顶点操作而较少判断两个特定顶点的关系时。
3、并查集(Union Find)
- 定义:用于处理一些不交集的合并和查询问题的数据结构。
- 判断任意两个元素是否属于同一个集合:
- 将集合在逻辑上表示为树结构,每个结点都指向其父结点,而树中的元素并无顺序之分,只要在同一棵树上,便说明在同一个集合中。
- 查找与合并:
- 查找:不断向上查找,直到找到它的根结点,之后根据根结点是否相同来判断两个元素是否属于同一集合。
- 合并:将两个子集合并成同一个集合。将一棵树作为另一棵树的子树,从而使得两棵树编程一棵更大的树。
- 优化方案:
- 路径压缩:在查找某个特定结点的根结点的同时,将其与根结点之间的所有结点都直接指向根结点。
- 合并时,将高度较低的树作为高度较高的树子树。
4、最小生成树(Minimum Spanning Tree,MST)
- 定义:
- 连通图:在一个无向图G中,若顶点u到顶点v有路径相连,则称u和v是连通的。若图中任意两点都是连通的,则图被称为连通图。
- 连通分量:无向图G中的一个极大连通子图称为G的一个连通分量。
- 连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。
- 生成树:在一个无向连通图G(V, E)中,如果存在一个连通子图,它包含所有顶点和部分边,且这个子图不存在回路,那么就称这个子图为原图的一棵生成树。
- 最小生成树:在带权无向连通图中,所有生成树中边权的和最小的那一棵(或几棵)被称为该无向图的最小生成树。
- 常见算法:
- Kruskal算法 + 并查集
- 步骤:
- 初试时所有顶点属于孤立的集合。
- 按照边权递增顺序遍历所有边,若遍历到的边的两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条),则确定该边为最小生成树上的一条边,并将该边两个顶点分属的集合合并。
- 遍历完所有边后,若原图连通,则被选取的边和所有顶点构成最小生成树;若原图不连通,最小生成树不存在。
- 步骤:
- Prim算法
- Kruskal算法 + 并查集
二、应用实例
1、题目描述:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?【浙江大学】
- 输入格式:测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。 - 输出格式:对每个测试用例,在1行里输出最少还需要建设的道路数目。
- 样例输入:
- 4 2
- 1 3
- 4 3
- 3 3
- 1 2
- 1 3
- 2 3
- 5 2
- 1 2
- 3 5
- 999 0
- 0
- 样例输出:
- 1
- 0
- 2
- 998
示例代码:
#include <iostream>
using namespace std;
const int MAX_N = 1000;
int father[MAX_N];
int height[MAX_N];
void Initial(int n){
for(int i = 0; i <= n; i++){
father[i] = i;
height[i] = 0;
}
}
int Find(int x){
while(x != father[x]){
x = father[x];
}
return father[x];
}
void Union(int x, int y){
x = Find(x);
y = Find(y);
if(x != y){
if(height[x] < height[y]){
father[x] = y;
}else if(height[x] > height[y]){
father[y] = x;
}else{
father[y] = x;
height[x]++;
}
}
}
int main(){
int n, m;
while(cin >> n && n != 0){
cin >> m;
Initial(n);
int x, y;
for(int i = 0; i < m; i++){
cin >> x >> y;
Union(x, y);
}
int count = -1;
for(int i = 1; i <= n; i++){
if(Find(i) == i){
count++;
}
}
cout << count << endl;
}
return 0;
}
2、题目描述:给定一个无向图和其中的所有边,判断这个图是否所有顶点都是连通的。【吉林大学】
- 输入格式:每组数据的第一行是两个整数 n 和 m(0<=n<=1000)。n 表示图的顶点数目,m 表示图中边的数目。随后有 m 行数据,每行有两个值 x 和 y(0<x, y <=n),表示顶点 x 和 y 相连,顶点的编号从 1 开始计算。输入不保证这些边是否重复。
- 输出格式:对于每组输入数据,如果所有顶点都是连通的,输出"YES",否则输出"NO"。
- 样例输入:
- 4 3
- 1 2
- 2 3
- 3 2
- 3 2
- 1 2
- 2 3
- 0 0
- 样例输出:
- NO
- YES
示例代码:
#include <iostream>
using namespace std;
const int MAX_N = 1001;
int height[MAX_N];
int father[MAX_N];
void Initial(int n){
for(int i = 0; i <= n; i++){
height[i] = 0;
father[i] = i;
}
}
int Find(int x){
while(x != father[x]){
x = father[x];
}
return father[x];
}
void Union(int x, int y){
x = Find(x);
y = Find(y);
if(x != y){
if(height[x] < height[y]){
father[x] = y;
}else if(height[x] > height[y]){
father[y] = x;
}else{
father[y] = x;
height[x]++;
}
}
}
int main(){
int n, m;
while(cin >> n >> m && n != 0){
Initial(n);
int x, y;
for(int i = 0; i < m; i++){
cin >> x >> y;
Union(x, y);
}
int result = -1;
bool flag = true;
for(int i = 1; i <= n; i++){
if(i == father[i]){
result++;
if(result > 0){
flag = false;
break;
}
}
}
if(flag){
cout << "YES" << endl;
}else{
cout << "NO" << endl;
}
}
return 0;
}
3、题目描述:A tree is a well-known data structure that is either empty (null, void, nothing) or is a set of on