带权图是一种图形结构,其中图中的边具有权重或成本。每条边连接两个顶点,并且具有一个与之关联的权重值,表示了两个顶点之间的某种度量、距离或成本。
带权图可以用邻接矩阵或邻接表来表示。邻接矩阵是一个二维矩阵,其中行和列表示图中的顶点,矩阵中的元素表示边的权重。邻接矩阵可以是一个二维数组,也可以是一个字典等其他数据结构。邻接表是一种以顶点为键的字典或映射数据结构,每个顶点关联一个边列表或链表,边列表中存储与该顶点相邻的顶点及其权重。
package WeightedGraph;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
/**
* 权重图类
* @author wushaopei
*/
public class WeightedGraph {
private TreeMap[] adj; // 邻接表数组
private int V,E; // 邻接表数组
/**
* 构造函数,根据文件名读取图的信息并创建权重图对象
* @param fileName 文件名
*/
public WeightedGraph(String fileName){
File file = new File(fileName);
try (Scanner scanner = new Scanner(file)){
V = scanner.nextInt(); // 读取顶点数
if (V < 0) throw new IllegalArgumentException("V must be non-negative");
E = scanner.nextInt(); // 读取边数
if (E < 0 ) throw new IllegalArgumentException("V must be non-negative");
adj = new TreeMap[V]; // 初始化邻接表数组
for (int i = 0; i < V; i++){
adj[i] = new TreeMap<>(); // 每个顶点关联一个邻接表
}
for (int j = 0; j < E; j ++){
int a = scanner.nextInt(); // 边的一个顶点
int b = scanner.nextInt(); // 边的一个顶点
int weight = scanner.nextInt(); // 边的一个顶点
if (a == b) throw new IllegalArgumentException("Self Loop is Detected!"); // 检测自环边
// 检测平行边
if (adj[a].containsKey(b)) throw new IllegalArgumentException("Parallel Edges are Detected!");
adj[a].put(b,weight); // 在邻接表中添加边及其权重
adj[b].put(a,weight);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* 验证顶点的有效性
* @param v 顶点
*/
public void validateVertex(int v){
if ( v < 0 && v > V){
throw new IllegalArgumentException("vertex " + v + " is invalid.");
}
}
/**
* 获取图的顶点数
* @return 顶点数
*/
public int V(){
return V;
}
/**
* 获取图的边数
* @return 边数
*/
private int E(){
return E;
}
/**
* 判断两个顶点之间是否存在边
* @param v 顶点v
* @param w 顶点w
* @return 是否存在边
*/
public boolean hashEdge(int v, int w){
validateVertex(v);
validateVertex(w);
return adj[v].containsKey(w);
}
/**
* 获取顶点的邻接链表
* @param v 顶点
* @return 邻接链表
*/
public Iterable adj(int v){
validateVertex(v);
return adj[v].keySet();
}
/**
* 获取两个顶点之间的权重
* @param v 顶点v
* @param w
*/
public int getWeighted(int v , int w){
if (hashEdge(v,w))
return adj[v].get(w);
throw new IllegalArgumentException(String.format("No edge %s-%s.", v, w));
}
/**
* 获取顶点的度数
* @param v 顶点
* @return 顶点的度数
*/
public int degree(int v){
validateVertex(v);
return adj[v].size();
}
/**
* 删除两个顶点之间的边
* @param v 顶点v
* @param w 顶点w
*/
public void removeEdge(int v, int w){
validateVertex(v);
validateVertex(w);
adj[v].remove(w);
adj[w].remove(v);
}
public static void main(String[] args) {
WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
System.out.println(weightedGraph.toString());
}
}
在带权图的实现中,常用的数据结构是邻接表(adjacency list)。邻接表是一个数组,数组的每个元素对应图中的一个顶点,每个顶点对应一个链表,链表中存储与该顶点相邻的顶点及其对应的权重。
具体实现中,可以使用以下数据结构来表示带权图:
顶点数和边数:使用变量 V
表示顶点数,使用变量 E
表示边数。
邻接表数组:使用一个数组 adj
来存储邻接表。adj
的长度为顶点数 V
,每个元素是一个 TreeMap
对象,用于存储与该顶点相邻的顶点和对应的权重。其中,TreeMap
是按照键的顺序进行排序的有序映射。
构造函数:构造函数用于从文件中读取图的信息并初始化邻接表。在构造函数中,读取文件的过程中可以完成以下操作:
adj
,为每个顶点创建一个空的 TreeMap
对象。通过使用邻接表来表示带权图,可以高效地进行图的遍历和相关操作。同时,邻接表的存储结构也适用于稀疏图,节省了存储空间。
@Override
protected Object clone() {
try {
WeightedGraph cloned = (WeightedGraph) super.clone();
cloned.adj = new TreeMap[V];
for (int v = 0; v < V; v ++){
cloned.adj[v] = new TreeMap();
for (Map.Entry entry: adj[v].entrySet()){
cloned.adj[v].put(entry.getKey(),entry.getValue());
}
}
return cloned;
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
return null;
}
/**
* 重写toString方法,打印图的信息
* @return 图的信息字符串
*/
@Override
public String toString() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(String.format("V = %d, E = %d\n",V,E));
for (int i = 0 ; i < V; i ++){
stringBuffer.append(String.format("%d : ",i));
for (Map.Entry entry: adj[i].entrySet()){
stringBuffer.append(String.format("(%d: %d)", entry.getKey(), entry.getValue()));
}
stringBuffer.append("\n");
}
return stringBuffer.toString();
}
测试带权图生成邻接表代码:
public static void main(String[] args) {
WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
System.out.println(weightedGraph.toString());
}
数:
7 12
0 1 2
0 3 7
0 5 2
1 2 1
1 3 4
1 4 3
1 5 5
2 4 4
2 5 4
3 4 1
3 6 5
4 6 7
生成的邻接表:
V = 7, E = 12
0 : (1: 2)(3: 7)(5: 2)
1 : (0: 2)(2: 1)(3: 4)(4: 3)(5: 5)
2 : (1: 1)(4: 4)(5: 4)
3 : (0: 7)(1: 4)(4: 1)(6: 5)
4 : (1: 3)(2: 4)(3: 1)(6: 7)
5 : (0: 2)(1: 5)(2: 4)
6 : (3: 5)(4: 7)
Process finished with exit code 0
生成树(Spanning Tree)是指在一个无向连通图中,选择部分边和顶点,构成一棵树,使得这棵树包含了图中的所有顶点,且不包含任何回路(环)。
生成树有以下特点:
上图是一个无向有权图,根据图可以知道,它有以下两种生成树:
最小生成树(Minimum Spanning Tree,简称 MST)是指在一个带权连通图中,找到一棵包含所有顶点的树,使得树的边权重之和最小。
常用的最小生成树算法有以下两种:
这两种算法都能得到最小生成树,但在不同的应用场景下可能有不同的性能表现。Prim算法适用于稠密图,而Kruskal算法适用于稀疏图。选择哪种算法取决于图的规模、稀疏程度以及算法的实现方式。
最小生成树算法在实际应用中有着广泛的应用,例如网络设计、电力传输、城市规划等领域。它可以帮助找到最优的连接方式或路径,以实现资源的最优利用和成本的最小化。
切分定理(Cut Property)是图论中的一个基本定理,它描述了生成树和图的边的关系。
切分定理的表述如下:
对于一个连通图,任意选择一个切分(Cut),即将图的顶点集合分为两个非空的子集,那么切分的边中权重最小的边必然属于该图的最小生成树。
换句话说,对于一个连通图,任意选择一个切分,最小生成树中的边必然包含了切分中的权重最小的边。
切分定理的证明可以通过反证法进行推导。假设最小生成树中不包含切分中的权重最小的边,那么可以通过添加该边来构造出一个权重更小的生成树,与最小生成树的定义相矛盾,因此切分定理成立。
切分定理在最小生成树算法中起到重要的作用。基于切分定理,一些最小生成树算法(如Prim算法)可以通过不断地选择切分中的最小权重边来构建最小生成树,从而提高算法的效率。切分定理也为理解生成树和图的关系提供了重要的理论基础。
在图论中,切分(Cut)和横切边(Crossing Edge)是相关的概念,它们描述了图中的边如何被分割和跨越切分的情况。
切分(Cut)是将图的顶点集合分为两个非空的子集的操作。一个切分将图的顶点集合分为两个部分,称为切分的两侧。如果顶点集合被划分为A和B两个子集,那么切分就可以表示为(A, B)。注意,切分只关注顶点集合的划分,并不关注具体的边。
横切边(Crossing Edge)是指连接切分的两侧的边,即一个顶点在切分的一侧,另一个顶点在切分的另一侧的边。换句话说,横切边横跨了切分的边界,连接了不同的子集。
切分与横切边在最小生成树算法中有重要的应用。切分定理指出,对于一个连通图的切分,最小生成树中的边必然包含切分中的权重最小的边。因此,在最小生成树算法(如Prim算法和Kruskal算法)中,通过选择切分中的最小权重边,可以逐步构建最小生成树。这样,横切边就是在切分过程中被添加到最小生成树中的边,它们连接了不同的子集。
总结起来,切分是将图的顶点集合划分为两个子集的操作,而横切边是连接切分的两侧的边。切分定理指出最小生成树中的边必然包含切分中的权重最小的边,因此在最小生成树算法中,通过选择切分中的最小权重边,可以逐步构建最小生成树,并将横切边添加到最小生成树中。
Kruskal算法是一种用于构建最小生成树的贪心算法。它的基本思想是从图中的边集合中逐步选择权重最小的边,并且确保所选择的边不会形成环路,直到最小生成树形成为止。
以下是Kruskal算法的步骤简述:
简而言之,Kruskal算法通过不断选择权重最小的边,并确保所选择的边不会形成环路,来构建最小生成树。它是一种贪心算法,每次选择当前最优的边,最终得到的最小生成树具有最小的总权重。
public class Kruskal {
private WeightedGraph G; // 带权图对象
private List edges; // 存储边的列表
public Kruskal(WeightedGraph G){
this.G = G;
CC cc = new CC(G); // 创建连通分量对象
if (cc.count() > 1){ // 如果图的连通分量数量大于1,即图不连通
throw new IllegalArgumentException("The graph is not connected.");
}
edges = new ArrayList<>(); // 初始化边的列表
// 遍历图的所有顶点
for (int v = 0; v < G.V(); v ++){
for (Integer w : G.adj(v)) { // 遍历顶点的邻接顶点
if (v < w)
edges.add(new WeightedEdge(v,w,G.getWeighted(v,w))); // 将边加入列表中
}
}
Collections.sort(edges); // 对边的列表按权重进行排序
}
}
/**
* 权重图类
* @author wushaopei
*/
public class WeightedEdge implements Comparable {
private int v,w,weight;
public WeightedEdge(int v, int w, int weight){
this.v = v;
this.w = w;
this.weight = weight;
}
@Override
public String toString() {
return String.format("%d:%d-%d", v,w,weight);
}
@Override
public int compareTo(WeightedEdge another) {
return weight - another.weight;
}
}
创建一个空的边集合,用于存储最小生成树的边。
并查集(Disjoint Set)是一种数据结构,用于处理集合的合并和查询操作。它主要用于解决一些与集合划分相关的问题,例如连通性判断、最小生成树的构建等。
在并查集中,每个元素都被看作是一个节点,并按照一定规则组织在集合中。每个节点都有一个指向父节点的指针,初始状态下,每个节点都是一个独立的集合,其父节点指向自身。
并查集主要包含以下几个操作:
初始化:创建一个并查集对象,指定元素的个数。初始状态下,每个元素都是一个独立的集合,其父节点指向自身。
查找根节点:通过递归查找的方式,找到某个节点所在集合的根节点。在查找过程中,将经过的所有节点的父节点指向根节点,以加速后续的查找操作,这一过程被称为路径压缩。
判断连通性:通过查找元素所在集合的根节点来判断两个元素是否属于同一个集合,即是否连通。
合并集合:将两个集合合并为一个集合,具体操作是将其中一个集合的根节点的父节点指向另一个集合的根节点。
在具体应用中,可以通过并查集实现一些常见的功能,例如判断图中的两个顶点是否连通、构建最小生成树等。在构建最小生成树时,可以按照边的权重从小到大进行排序,然后依次加入边,如果加入的边的两个顶点不属于同一个集合(即不连通),则将它们合并,并将边加入最小生成树的边集合中。
这种基于并查集的思想可以有效地处理图的连通性问题,具有较高的效率。通过路径压缩和按秩合并等优化策略,可以进一步提高并查集的性能。
package WeightedGraph;
/**
* @author wushaopei
*/
public class UF{
private int[] parent;
public UF(int n){
parent = new int[n];
for(int i = 0 ; i < n ; i ++)
parent[i] = i;
}
public int find(int p){
if( p != parent[p] )
parent[p] = find( parent[p] );
return parent[p];
}
public boolean isConnected(int p , int q){
System.out.println(find(p) +"-"+ find(q));
return find(p) == find(q);
}
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
在构造并查集对象时,通过循环将每个顶点的父节点初始化为自身,即 parent[i] = i
,这样每个顶点就形成了独立的集合。
当调用 find(p)
方法时,如果 p
不是根节点(即 p != parent[p]
),则通过递归调用 find( parent[p] )
来查找 p
的根节点。在这个过程中,会沿着顶点的父节点一直向上查找,直到找到根节点。最后,将经过的所有顶点的父节点都指向根节点,从而实现路径压缩,加快后续查找的速度。
因此,当执行 find(p)
和 find(q)
时,会分别返回 p
和 q
的根节点。如果它们的根节点不相同,即 find(p) != find(q)
,则说明 p
和 q
不属于同一个连通分量,也就是不联通。
当调用 unionElements(p, q)
方法时,会先通过 find(p)
和 find(q)
分别找到 p
和 q
的根节点,然后将其中一个根节点的父节点指向另一个根节点,从而将两个集合合并为一个集合。
这样,在使用并查集判断两个顶点是否连通时,通过比较它们的根节点是否相同,即 find(p) == find(q)
,来判断它们是否属于同一个连通分量。如果它们的根节点相同,说明它们是连通的;如果根节点不同,说明它们不连通。
package WeightedGraph;
import java.util.*;
/**
* @author wushaopei
* @create 2023-06-06 13:41
*/
public class Kruskal {
private WeightedGraph G; // 带权图对象
private ArrayList edges; // 存储边的列表
private ArrayList mst;
public Kruskal(WeightedGraph G){
this.G = G;
CC cc = new CC(G); // 创建连通分量对象
if (cc.count() > 1){ // 如果图的连通分量数量大于1,即图不连通
throw new IllegalArgumentException("The graph is not connected.");
}
edges = new ArrayList<>(); // 初始化边的列表
// 遍历图的所有顶点
for (int v = 0; v < G.V(); v ++){
for (Integer w : G.adj(v)) { // 遍历顶点的邻接顶点
if (v < w)
edges.add(new WeightedEdge(v,w,G.getWeighted(v,w))); // 将边加入列表中
}
}
Collections.sort(edges); // 对边的列表按权重进行排序
UF uf = new UF(G.V());
mst = new ArrayList();
for (WeightedEdge edge : edges) {
int v = edge.getV();
int w = edge.getW();
if (!uf.isConnected(v,w)){
mst.add(edge);
uf.unionElements(v,w);
}
}
}
public ArrayList result(){
return mst;
}
public static void main(String[] args) {
WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
Kruskal kruskal = new Kruskal(weightedGraph);
System.out.println(kruskal.result());
}
}
执行结果:
[1-2: 1, 3-4: 1, 0-1: 2, 0-5: 2, 1-4: 3, 3-6: 5]
时间复杂度: O(ElogE)
Kruskal算法是一种常用于解决最小生成树问题的贪心算法。它通过逐步选择图中权重最小的边,并保证边的选择不会形成环路,从而构建最小生成树。
以下是Kruskal算法的实现步骤:
Kruskal算法的核心思想是根据边的权重逐步选择边,并确保所选的边不会形成环路。通过并查集的帮助,可以高效地判断两个顶点是否连通,并避免选择形成环路的边。
Prim算法是一种常用于解决最小生成树问题的贪心算法。它从一个起始顶点开始,逐步扩展最小生成树的边集合,直到覆盖所有顶点。
以下是Prim算法的实现步骤:
Prim算法的核心思想是通过选择权重最小的边来逐步扩展最小生成树,保证每一步加入的边都是连接已经覆盖的顶点和未覆盖的顶点的最短边。通过不断更新候选边列表edges和最小生成树边列表mst,最终得到最小生成树。
需要注意的是,Prim算法可以使用不同的数据结构来实现,如优先队列(最小堆)、堆、红黑树等,以便高效地选择权重最小的边。具体的实现方式可能因编程语言和数据结构的选择而有所差异,但基本思想是相同的。
操作切分,从1:V-1 开始,每次找当前切分的最短横切边;扩展切分,直到没有切分。
package WeightedGraph;
import java.util.ArrayList;
import java.util.Collections;
/**
* @author wushaopei
*/
public class Prim {
private WeightedGraph G; // 带权图对象
private ArrayList edges; // 存储边的列表
private ArrayList mst;
public Prim(WeightedGraph G){
this.G = G;
mst = new ArrayList();
CC cc = new CC(G); // 创建连通分量对象
if (cc.count() > 1){ // 如果图的连通分量数量大于1,即图不连通
throw new IllegalArgumentException("The graph is not connected.");
}
edges = new ArrayList<>(); // 初始化边的列表
// 遍历图的所有顶点
for (int v = 0; v < G.V(); v ++){
for (Integer w : G.adj(v)) { // 遍历顶点的邻接顶点
if (v < w)
edges.add(new WeightedEdge(v,w,G.getWeighted(v,w))); // 将边加入列表中
}
}
Collections.sort(edges); // 对边的列表按权重进行排序
UF uf = new UF(G.V());
boolean[] visited = new boolean[G.V()];
visited[0] = true;
// 顶点数
for (int i = 0 ; i < G.V(); i ++){
WeightedEdge minWeightedEdge = new WeightedEdge(-1,-1, Integer.MAX_VALUE);
// 扫描所有边
for ( int v = 0; v < G.V(); v ++){
if (visited[v]){
for (Integer w : G.adj(v)) {
if ( !visited[w] && G.getWeighted(v,w) < minWeightedEdge.getWeight()){
minWeightedEdge = new WeightedEdge(v,w,G.getWeighted(v,w));
}
}
}
}
if (minWeightedEdge.getWeight() != Integer.MAX_VALUE){
mst.add(minWeightedEdge);
visited[minWeightedEdge.getV()] = true;
visited[minWeightedEdge.getW()] = true;
}
}
}
public ArrayList result(){
return mst;
}
public static void main(String[] args) {
WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
Prim kruskal = new Prim(weightedGraph);
System.out.println(kruskal.result());
}
}
时间复杂度:O((V-1) * (V+E)) = O(VE)
(1)初始化一个空的最小生成树边列表 `mst`。
(2)创建连通分量对象 `cc`,并检查图是否连通。如果图的连通分量数量大于1,则抛出异常,表示图不连通。
(3)初始化边的列表 `edges`。
(4)遍历图的所有顶点,对于每个顶点 `v`,遍历其邻接顶点 `w`:
- 如果顶点 `v` 的索引小于顶点 `w` 的索引,则将边 `(v, w)` 加入边列表 `edges`。
(5) 对边列表 `edges` 按权重进行排序。
(6)创建并查集对象 `uf`。
(7)创建布尔数组 `visited`,用于记录顶点是否被访问。将起始顶点(例如索引为 0 的顶点)标记为已访问。
(8)创建优先队列 `pq`(优先级队列),用于存储边的权值。
(9)遍历起始顶点的邻接顶点,将与起始顶点相连的边 `(0, w)` 加入优先队列 `pq`。
(10)循环直到优先队列 `pq` 为空:
- 从优先队列 `pq` 中取出权值最小的边 `minEdge`。
- 如果边 `minEdge` 的两个顶点都已经被访问过,则跳过当前循环。
- 将边 `minEdge` 加入最小生成树的边列表 `mst`。
- 获取边 `minEdge` 的未访问顶点 `newv`(如果 `minEdge` 的顶点 `v` 已访问,则取顶点 `w`;如果顶点 `w` 已访问,则取顶点 `v`)。
- 将顶点 `newv` 标记为已访问。
- 遍历顶点 `newv` 的邻接顶点 `w`,如果顶点 `w` 未访问,则将边 `(newv, w)` 加入优先队列 `pq`。
(11)返回最小生成树的边列表 `mst`。
该算法利用了优先队列的特性,每次选择权值最小的边来扩展最小生成树的边集合,直到所有顶点都被访问过。这种优化减少了不必要的遍历和比较操作,提高了算法的效率。
public PrimQueue(WeightedGraph G){
this.G = G;
mst = new ArrayList();
CC cc = new CC(G); // 创建连通分量对象
if (cc.count() > 1){ // 如果图的连通分量数量大于1,即图不连通
throw new IllegalArgumentException("The graph is not connected.");
}
edges = new ArrayList<>(); // 初始化边的列表
// 遍历图的所有顶点
for (int v = 0; v < G.V(); v ++){
for (Integer w : G.adj(v)) { // 遍历顶点的邻接顶点
if (v < w)
edges.add(new WeightedEdge(v,w,G.getWeighted(v,w))); // 将边加入列表中
}
}
Collections.sort(edges); // 对边的列表按权重进行排序
UF uf = new UF(G.V());
boolean[] visited = new boolean[G.V()];
visited[0] = true;
// 顶点数
Queue pq = new PriorityQueue();
for (int w:G.adj(0)){
pq.add(new WeightedEdge(0,w, G.getWeighted(0,w)));
}
while (!pq.isEmpty()){
WeightedEdge minEdge = (WeightedEdge)pq.remove();
if (visited[minEdge.getV()] && visited[minEdge.getW()]){
continue;
}
mst.add(minEdge);
int newv = visited[minEdge.getV()]?minEdge.getW():minEdge.getV();
visited[newv] = true;
for (int w : G.adj(newv)) {
if (!visited[w]){
pq.add(new WeightedEdge(newv,w,G.getWeighted(newv,w)));
}
}
}
}
时间复杂度:O(ElogE)