【Data Structure and Algorithm】Graph Theory and Related Algorithms

Basic introduction to graphs

A linear table is limited to a relationship between a direct predecessor and a direct successor, and a tree can only have one direct predecessor, that is, a parent node. When we need to represent a many-to-many relationship, we use a graph here.

A graph is a data structure in which a node can have zero or more adjacent elements. A connection between two nodes is called an edge. Nodes can also be called vertices. As shown in the picture:

insert image description here

In simple terms, a graph is a set consisting of a finite nonempty set of vertices and edges between them. Usually expressed as: G(V,E), where G represents a graph, V represents a collection of vertices, and E represents a collection of edges.

Then we talk about some common concepts in the figure:

  1. 节点(Vertex): The basic element in the diagram, used to represent an entity.

  2. 边(Edge): A line segment connecting two nodes, used to represent the relationship between nodes.

  3. 度(Degree): The degree indicates how many edges a vertex contains. In a directed graph, it is also divided into out-degree and in-degree. The out-degree indicates the number of edges going out from the vertex, and the in-degree indicates the number of edges entering the vertex.

  4. 有向图和无向图: Whether the edge has direction determines whether the graph is directed or undirected. The directed edge indicates the starting point and the ending point of the edge, and the two nodes of the undirected edge have no distinction between the starting point and the ending point.
    insert image description here

  5. 带权图和无权图: Whether the edge has a weight value determines whether the graph is a weighted graph or an unweighted graph. The weighted edge has a value, which represents the weight of the edge, and the weight of the unweighted edge is regarded as 1.
    insert image description here

  6. 路径: A sequence of edges between a series of vertices in a graph is called a path.

  7. 连通性: A graph is connected if there exists a path between any two nodes. Otherwise it is a disconnected graph.

  8. 海量图: The number of nodes and edges is huge, far beyond the size of computer memory, and requires special algorithms and storage structures to process graphs.

representation of graph

There are two ways to represent graphs:

  • Two-dimensional array representation (adjacency matrix)
  • Linked list representation (adjacency list)

adjacency matrix

Adjacency matrix is ​​a storage structure of graph, which uses a two-dimensional array to represent the edges between nodes in the graph.

insert image description here
Specifically:

  1. The number of rows and columns of the adjacency matrix is ​​the number n of nodes in the graph.
  2. Each element in the matrix represents an edge. If there is an edge between node i and node j, the element in row i and column j of the matrix is ​​1, otherwise it is 0.
  3. For weighted graphs, the elements of the matrix are the corresponding weights, not 1.
  4. The elements on the main diagonal of the matrix are all 0, because the node will not be connected to itself.
  5. The adjacency matrix is ​​suitable for representing dense graphs, and the space complexity is O(n2). If the graph is sparse, the space utilization is relatively low.

The advantages of an adjacency matrix are:

  1. It can be determined in O(1) time whether there is an edge between any two nodes.
  2. It is convenient to implement some algorithms, such as breadth-first search, depth-first search, etc.

The disadvantages of an adjacency matrix are:

  1. The space complexity is high, and if the graph is sparse, it will cause space waste.
  2. It is difficult to represent a dynamic graph. If edges are frequently added and deleted, the adjacency matrix needs to be frequently rebuilt.

adjacency list

Adjacency list is another storage structure of graph, which uses linked list to store the adjacent nodes of each node.

insert image description here

Specifically:

  1. Adjacency lists are composed of arrays and linked lists. Each element of the array is a linked list, which represents the list of adjacent nodes of the corresponding node.
  2. The index of the array is the serial number of the node, and the size of the array is the number of nodes n.
  3. If node i has an edge with node j, add j to the linked list corresponding to i.
  4. For weighted graphs, the elements of each node in the linked list no longer only store the serial number of the adjacent node, but store a structure, including the serial number of the adjacent node and the corresponding weight.
  5. The adjacency list is suitable for sparse graphs, and the space complexity is O(n+e), where e is the number of edges. For dense graphs, the space utilization rate will be higher.

The advantages of the adjacency list are:
6. The space complexity is low, and it is suitable for representing sparse graphs.
7. It is easy to realize the operation of adding and deleting dynamic graphs.

The disadvantages of the adjacency list are:
8. It is difficult to judge whether there is an edge between any two nodes in O(1) time, and the linked list needs to be traversed.
9. It is not convenient to implement some algorithms, such as breadth-first search and depth-first search.

Depth-first traversal (DFS) of graphs

The so-called graph traversal is the access to the nodes. A graph has so many nodes, how to traverse these nodes requires a specific strategy, generally there are two access strategies:

  • 深度优先遍历(DFS)
  • 广度优先遍历(BFS)

overview

Depth-first traversal, starting from the initial access node, the initial access node may have multiple adjacent nodes, the strategy of depth-first traversal is to first visit the first adjacent node, and then use the visited adjacent node as the initial node to visit its first adjacent node.

It can be understood as follows: every time after visiting the current node, first visit the first adjacent node of the current node. We can see that such an access strategy is to dig deeper vertically instead of horizontally accessing all adjacent nodes of a node. Obviously, depth-first search is a recursive process

insert image description here

In general: Depth-First Search (Depth-First Search) is a graph traversal algorithm. It starts from a certain node and searches as deeply as possible until all nodes on the current path are traversed. Then backtrack, continuing to search the next path as deep as possible .

The main application of depth-first search isGraph connectivity check, topological sorting, solving the number of cuts and solving the maximum matching number of bipartite graphs, etc.

Implementation steps

The process of depth-first search can be implemented recursively. The main steps are as follows:

  1. Starting from a node v in the target graph, visit the node.
  2. If v's neighbor node w has not been visited, recursively visit w.
  3. If w has been visited, go back to node v and visit another unvisited adjacent node of v.
  4. If all adjacent nodes of v have been visited, backtrack to the previous node of v.
  5. Repeat steps 3 and 4 until all nodes are visited.

Since recursion is used, we can definitely use the stack idea to understand:

insert image description here
insert image description here

insert image description here
insert image description here

insert image description here
insert image description here

Code

insert image description here

public class Graph {
    
    

	//存储顶点集合
	private ArrayList<String> vertexList; 
	//存储图对应的邻结矩阵
	private int[][] edges; 
	 //表示边的数目
	private int numOfEdges;
	//定义给数组 boolean[], 记录某个结点是否被访问
	private boolean[] isVisited;

    //dfs部分
    public void dfs(){
    
    
        //初始化访问数组
        isVisited = new boolean[vertexList.size()];
        for (int i = 0;i < isVisited.length;i++) {
    
    
            if (!isVisited[i]) {
    
    
                dfs(i);
            }
        }
    }

    public void dfs(int index){
    
    
        //打印出当前节点
        System.out.print(getValueByIndex(index) + " -> ");
        //设置当前节点已被访问
        isVisited[index] = true;
        //找出该节点的第一个邻接点
        int firstNeighbor = getFirstNeighbor(index);
        //说明存在第一个临界点
        if (firstNeighbor != -1) {
    
    
            dfs(firstNeighbor);
        }
    }

    /**
     * 找到index节点的第一个临界点,如果没有返回-1
     * @param index
     * @return
     */
    public int getFirstNeighbor(int index) {
    
    
        for (int i = 0; i < edges.length; i++) {
    
    
            if (edges[index][i] == 1 && !isVisited[i]) {
    
    
                return i;
            }
        }
        return -1;
    }
	
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	

}

Notice:

  • In our getFirstNeighbor method, the j here must start from 0, and some people tend to start with index+1, which does not take into account the situation that the traversal does not start from A.
  • The reason for traversing our node list is that it may be a disconnected graph, and traversing from a node may not traverse the nodes of the entire graph to
    insert image description here

Breadth-first traversal (BFS) of graphs

overview

Breadth-First Search is a graph traversal algorithm. Starting from a certain node, it first visits all the nodes adjacent to the node, then visits the adjacent nodes of the adjacent node, and so on, until all nodes are traversed.

Breadth-first search expands outward layer by layer like ripples on the water:
insert image description here

Compared with depth-first search, the characteristics of breadth-first search are:

  1. Nodes closer to the starting node will be visited first. Depth-first search will search as deep as possible, possibly farther from the starting point.
  2. Implemented using a queue, the space complexity is high. Depth-first search uses a recursive stack with low space complexity.

Breadth-first search is mainly used forThe shortest path problem in graphs, topological sortingwait.

Implementation steps

The process of breadth-first search can be implemented with a queue. The main steps are as follows:

  1. Starting from a certain node v in the graph, visit the node and enqueue it.
  2. Take out the head node of the team, visit all unvisited adjacent nodes of this node, and enqueue the adjacent nodes.
  3. Repeat step 2 until the queue is empty.
  4. If there are unvisited nodes in the graph, start from one of the unvisited nodes and repeat steps 1~3.

insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here

Code

insert image description here

public class Graph {
    
    

	//存储顶点集合
	private ArrayList<String> vertexList; 
	//存储图对应的邻结矩阵
	private int[][] edges; 
	 //表示边的数目
	private int numOfEdges;
	//定义给数组 boolean[], 记录某个结点是否被访问
	private boolean[] isVisited;

    /**
     * 在bfs中负责找到指定节点的所有未遍历节点
     * @param index
     */
    public void getNeighbors(int index, Queue<Integer> queue){
    
    
        for (int i = 0; i < edges.length; i++) {
    
    
            if (edges[index][i] == 1 && !isVisited[i]) {
    
    
                queue.add(i);
                //将入队节点标为已访问
                isVisited[i] = true;
            }
        }
    }

    public void bfs(){
    
    
        //初始化访问数组
        isVisited = new boolean[vertexList.size()];
        for (int i = 0;i < isVisited.length;i++) {
    
    
            if (!isVisited[i]) {
    
    
                bfs(i);
            }
        }
    }

    public void bfs(int index) {
    
    
        //创建队列
        Queue<Integer> queue = new LinkedList<>();
        //首先将起始节点加入队列,并设为已访问
        queue.add(index);
        isVisited[index] = true;
        //每次弹出的队头
        Integer head;
        while (!queue.isEmpty()) {
    
    
            //弹出头节点
            head = queue.poll();
            //并将其临界点全部放入队列
            getNeighbors(head,queue);
            //打印该节点
            System.out.print(getValueByIndex(head) + " -> ");
        }

    }
	
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	
	//返回结点的个数
	public int getNumOfVertex() {
    
    
		return vertexList.size();
	}


}

Summary of common codes for graphs

public class Graph {
    
    

	private ArrayList<String> vertexList; //存储顶点集合
	private int[][] edges; //存储图对应的邻结矩阵
	private int numOfEdges; //表示边的数目
	private boolean[] isVisited; //记录某个结点是否被访问
	
	public static void main(String[] args) {
    
    
		//测试一把图是否创建ok
		int n = 8;  //结点的个数
		//String Vertexs[] = {"A", "B", "C", "D", "E"};
		String Vertexs[] = {
    
    "1", "2", "3", "4", "5", "6", "7", "8"};
		
		//创建图对象
		Graph graph = new Graph(n);
		//循环的添加顶点
		for(String vertex: Vertexs) {
    
    
			graph.insertVertex(vertex);
		}
		
		//添加边
		//A-B A-C B-C B-D B-E 
//		graph.insertEdge(0, 1, 1); // A-B
//		graph.insertEdge(0, 2, 1); // 
//		graph.insertEdge(1, 2, 1); // 
//		graph.insertEdge(1, 3, 1); // 
//		graph.insertEdge(1, 4, 1); // 
		
		//更新边的关系
		graph.insertEdge(0, 1, 1);
		graph.insertEdge(0, 2, 1);
		graph.insertEdge(1, 3, 1);
		graph.insertEdge(1, 4, 1);
		graph.insertEdge(3, 7, 1);
		graph.insertEdge(4, 7, 1);
		graph.insertEdge(2, 5, 1);
		graph.insertEdge(2, 6, 1);
		graph.insertEdge(5, 6, 1);

		
		
		//显示一把邻结矩阵
		graph.showGraph();
		
		//测试一把,我们的dfs遍历是否ok
		System.out.println("深度遍历");
		graph.dfs(); // A->B->C->D->E [1->2->4->8->5->3->6->7]
//		System.out.println();
		System.out.println("广度优先!");
		graph.bfs(); // A->B->C->D-E [1->2->3->4->5->6->7->8]
		
	}
	
	//构造器
	public Graph(int n) {
    
    
		//初始化矩阵和vertexList
		edges = new int[n][n];
		vertexList = new ArrayList<String>(n);
		numOfEdges = 0;
		
	}
	
	//得到第一个邻接结点的下标 w 
	/**
	 * 
	 * @param index 
	 * @return 如果存在就返回对应的下标,否则返回-1
	 */
	public int getFirstNeighbor(int index) {
    
    
		for(int j = 0; j < vertexList.size(); j++) {
    
    
			if(edges[index][j] > 0) {
    
    
				return j;
			}
		}
		return -1;
	}
	//根据前一个邻接结点的下标来获取下一个邻接结点
	public int getNextNeighbor(int v1, int v2) {
    
    
		for(int j = v2 + 1; j < vertexList.size(); j++) {
    
    
			if(edges[v1][j] > 0) {
    
    
				return j;
			}
		}
		return -1;
	}
	
	//深度优先遍历算法
	//i 第一次就是 0
	private void dfs(boolean[] isVisited, int i) {
    
    
		//首先我们访问该结点,输出
		System.out.print(getValueByIndex(i) + "->");
		//将结点设置为已经访问
		isVisited[i] = true;
		//查找结点i的第一个邻接结点w
		int w = getFirstNeighbor(i);
		while(w != -1) {
    
    //说明有
			if(!isVisited[w]) {
    
    
				dfs(isVisited, w);
			}
			//如果w结点已经被访问过
			w = getNextNeighbor(i, w);
		}
		
	}
	
	//对dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
	public void dfs() {
    
    
		isVisited = new boolean[vertexList.size()];
		//遍历所有的结点,进行dfs[回溯]
		for(int i = 0; i < getNumOfVertex(); i++) {
    
    
			if(!isVisited[i]) {
    
    
				dfs(isVisited, i);
			}
		}
	}
	
	//对一个结点进行广度优先遍历的方法
	private void bfs(boolean[] isVisited, int i) {
    
    
		int u ; // 表示队列的头结点对应下标
		int w ; // 邻接结点w
		//队列,记录结点访问的顺序
		LinkedList queue = new LinkedList();
		//访问结点,输出结点信息
		System.out.print(getValueByIndex(i) + "=>");
		//标记为已访问
		isVisited[i] = true;
		//将结点加入队列
		queue.addLast(i);
		
		while( !queue.isEmpty()) {
    
    
			//取出队列的头结点下标
			u = (Integer)queue.removeFirst();
			//得到第一个邻接结点的下标 w 
			w = getFirstNeighbor(u);
			while(w != -1) {
    
    //找到
				//是否访问过
				if(!isVisited[w]) {
    
    
					System.out.print(getValueByIndex(w) + "=>");
					//标记已经访问
					isVisited[w] = true;
					//入队
					queue.addLast(w);
				}
				//以u为前驱点,找w后面的下一个邻结点
				w = getNextNeighbor(u, w); //体现出我们的广度优先
			}
		}
		
	} 
	
	//遍历所有的结点,都进行广度优先搜索
	public void bfs() {
    
    
		isVisited = new boolean[vertexList.size()];
		for(int i = 0; i < getNumOfVertex(); i++) {
    
    
			if(!isVisited[i]) {
    
    
				bfs(isVisited, i);
			}
		}
	}
	
	//图中常用的方法
	//返回结点的个数
	public int getNumOfVertex() {
    
    
		return vertexList.size();
	}
	//显示图对应的矩阵
	public void showGraph() {
    
    
		for(int[] link : edges) {
    
    
			System.err.println(Arrays.toString(link));
		}
	}
	//得到边的数目
	public int getNumOfEdges() {
    
    
		return numOfEdges;
	}
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	//返回v1和v2的权值
	public int getWeight(int v1, int v2) {
    
    
		return edges[v1][v2];
	}
	//插入结点
	public void insertVertex(String vertex) {
    
    
		vertexList.add(vertex);
	}
	//添加边
	/**
	 * 
	 * @param v1 表示点的下标即使第几个顶点  "A"-"B" "A"->0 "B"->1
	 * @param v2 第二个顶点对应的下标
	 * @param weight 表示 
	 */
	public void insertEdge(int v1, int v2, int weight) {
    
    
		edges[v1][v2] = weight;
		edges[v2][v1] = weight;
		numOfEdges++;
	}
}

Minimum Spanning Tree Algorithm

Minimum spanning tree is a very important concept in graph theory.It refers to a tree connecting all nodes in the graph, and the sum of the weights of all edges on this tree is the smallest

A minimum spanning tree has several important properties:

  1. It contains all nodes in the graph, there are no isolated points.
  2. It is a tree, without rings.
  3. Its sum of weights is the smallest.

insert image description here

Common minimum spanning tree algorithms are:
4. Prim算法: Start from a node, and continuously add new nodes and edges to the spanning tree until all nodes are included. Each new edge added is the shortest edge to a node in the tree.
5. Kruskal算法: Select the edge according to the weight of the edge from small to large. As long as this edge does not form a ring, it will be added to the minimum spanning tree.
6. Dijkstra算法: Use Dijkstra's shortest path algorithm to find the shortest path from each node to all other nodes, and the minimum spanning tree is formed by these shortest paths.

The minimum spanning tree has many practical applications, such as network connectivity, circuit wiring and so on. It provides an efficient solution to these practical problems.

In short, the minimum spanning tree is a very classic and important concept in graph theory, and related algorithms are also important, and it is worth understanding and mastering.

Minimum spanning tree (Kruskal (Kruskal) and Prim (Prim)) algorithm animation demonstration

Prim's Algorithm

Prim's algorithm is one of the classic algorithms of minimum spanning tree. Its basic idea is:

  • Starting from any node in the graph, add edges and nodes step by step to form a minimum spanning tree. Each new edge added must connect nodes in the tree and nodes not in the tree, and this new edge must be the one with the smallest weight among all candidate edges.

The steps of Prim's algorithm are as follows:

  1. Select any node in the graph as the starting node, and mark that node has been visited.
  2. Find an edge with the smallest weight among all unvisited nodes connected to the visited nodes. Unvisited nodes connected by this edge are marked as visited.
  3. Repeat step 2 until all nodes are visited.
  4. The edge set formed is the minimum spanning tree.

Or you can refer to the following video which explains it very clearly:

Algorithm diagram:
insert image description here

The time complexity of Prim's algorithm is O(n2), and it can be reduced to O(nlogn) if it is implemented with a priority queue.

Prim's algorithm is only applicable to weighted undirected connected graphs. If the graph is directed or disconnected, Prim's algorithm cannot obtain the minimum spanning tree.

algorithm practice

Code:

import java.util.*;

public class PrimAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int prim(int[][] graph) {
    
    
        int n = graph.length;
        //创建距离表,代表索引对应节点距离最小生成树的距离
        int[] dist = new int[n];
        //记录每个节点是否被添加,未添加记为false
        boolean[] visited = new boolean[n];
        //先将距离表都初始化为最大值
        Arrays.fill(dist, INF);
        //从0节点开始遍历,将距离更新一下
        dist[0] = 0;
        //记录要返回的最小权值
        int res = 0;
		
        for (int i = 0; i < n; i++) {
    
    
        	//u代表要添加的节点(距离最近且未访问)
            int u = -1;
            for (int j = 0; j < n; j++) {
    
    
                if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
    
    
                    u = j;
                }
            }
            //将要添加的节点标为已访问
            visited[u] = true;
            //记录权值
            res += dist[u];
            //更新距离表
            for (int v = 0; v < n; v++) {
    
    
                if (!visited[v] && graph[u][v] != INF && graph[u][v] < dist[v]) {
    
    
                    dist[v] = graph[u][v];
                }
            }
        }

        return res;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, 8, 5},
                {
    
    INF, 3, 0, INF, 7},
                {
    
    6, 8, INF, 0, 9},
                {
    
    INF, 5, 7, 9, 0}
        };
        int res = prim(graph);
        System.out.println(res);
    }
}

In this implementation, we first initialize the distance array dist and the visit flag array visited, and initialize all elements of the distance array to infinity (representing nodes that have not been visited).

Then, we traverse all nodes starting from node 0, each time selecting the smallest value in the distance array as the next node to be visited. Then, we mark this node as visited, and update its distance to the not-yet-visited nodes in the distance array. Finally, we return the sum of all edge weights as the minimum spanning tree weight.

The time complexity of this implementation is O(n^2), where n is the number of nodes.

Kruskal algorithm

Kruskal's algorithm is another classic algorithm for minimum spanning tree. Its basic idea is:
sort all the edges in the graph according to their weights from small to large. Select the edge with the smallest weight, and add it to the minimum spanning tree as long as the edge does not form a cycle. Repeat this step until the minimum spanning tree contains all the nodes in the graph.

The steps of Kruskal algorithm are as follows:

  1. Sort all the edges in the graph in ascending order of weight.
  2. Select the edge with the smallest weight to determine whether a ring is formed. If it does not form a cycle, it is added to the minimum spanning tree.
  3. Repeat step 2 until the minimum spanning tree contains all vertices in the graph.
  4. Output a minimum spanning tree.

The time complexity of Kruskal's algorithm is O(ElogE), where E is the number of edges in the graph.

The implementation of the Kruskal algorithm needs to use the union search set to judge whether the selected edge will form a ring . Union check can judge whether two elements belong to the same set in O(1) time, which is the key to realize Kruskal algorithm.

Compared with Prim's algorithm, Kruskal's algorithm is suitable for sparse graphs with more edges, because its time complexity does not depend on the number of nodes, but only on the number of edges. But the Kruskal algorithm needs to sort all the edges in advance, which increases the space complexity.

Illustration:
insert image description here

And lookup

Union check set is a tree-type data structure, which is used to deal with the merging and query problems of some disjoint sets .
It supports three operations:

insert image description here

  1. initialize init
  2. union(x, y): Merge the collection where element x and element y are located.
  3. find(x): Find the representative of the set where the element x is located, and the representative of the set is the earliest element added to the set.

The union search implements a kind of dynamic connectivity. At the beginning, each element forms a set by itself, and the sets are continuously merged through the union operation, and finally several disjoint large sets are formed.

The typical application of union search is to solve the offline query problem in graph theory, such as querying whether two nodes are in the same connected graph in an undirected graph.

There are two common ways to implement union check:

  1. Quick search: Using a tree structure, the find operation needs to traverse to the root node, and the time complexity is O(n).
  2. Fast merge with path compression: During the traversal process of the find operation, point the node directly to the root node to achieve path compression and reduce the depth of the tree. After balancing, the time complexity can reach O(1).

insert image description here
insert image description here

algorithm practice

import java.util.*;

public class KruskalAlgorithm {
    
    
    private static class Edge implements Comparable<Edge> {
    
    
        int u, v, w;

        public Edge(int u, int v, int w) {
    
    
            this.u = u;
            this.v = v;
            this.w = w;
        }

        @Override
        public int compareTo(Edge o) {
    
    
            return Integer.compare(this.w, o.w);
        }
    }

    public static int kruskal(int[][] graph) {
    
    
        int n = graph.length;
        List<Edge> edges = new ArrayList<>();
        for (int u = 0; u < n; u++) {
    
    
            for (int v = u + 1; v < n; v++) {
    
    
                if (graph[u][v] != 0) {
    
    
                    edges.add(new Edge(u, v, graph[u][v]));
                }
            }
        }
        Collections.sort(edges);
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            parent[i] = i;
        }
        int res = 0;
        for (Edge edge : edges) {
    
    
            int u = edge.u;
            int v = edge.v;
            int w = edge.w;
            int pu = find(parent, u);
            int pv = find(parent, v);
            if (pu != pv) {
    
    
                parent[pu] = pv;
                res += w;
            }
        }
        return res;
    }

    private static int find(int[] parent, int x) {
    
    
        if (parent[x] != x) {
    
    
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, 0, 6, 0},
                {
    
    2, 0, 3, 8, 5},
                {
    
    0, 3, 0, 0, 7},
                {
    
    6, 8, 0, 0, 9},
                {
    
    0, 5, 7, 9, 0}
        };
        int res = kruskal(graph);
        System.out.println(res);
    }
}

In this implementation, we first store all the edges in the graph in a list, and sort them in ascending order of weight. Then, we initialize a union search, initializing each node's parent to itself.

Next, we traverse the sorted edge list. For each edge, if its two endpoints are not in the same connected block, merge them into the same connected block, and add the weight of this edge to the weight of the minimum spanning tree.

The time complexity of this implementation is O(m log m), where m is the number of edges.

shortest path algorithm

Graph theory shortest distance (Shortest Path) algorithm animation demonstration - Dijkstra (Dijkstra) and Floyd (Floyd)

Dijkstra's Algorithm

Dijkstra's algorithm is a typical shortest path algorithm, which is used to calculate the shortest path from a node to other nodes. Its main feature is to expand from the starting point to the outer layer (breadth-first search idea) until it reaches the end.

Dijkstra's algorithm is an algorithm for finding the single-source shortest path. Its basic idea is:

  1. Select a node as the starting node, and calculate the shortest path from the starting node to other nodes.
  2. Traverse all nodes reachable from the starting node, and update the shortest path. Select the next node to continue traversing until all nodes are traversed.
  3. Repeat the above steps until the shortest path from the starting node to all nodes is finally obtained.

The steps of Dijkstra's algorithm are as follows:

  1. Select the starting node source, and set its distance to 0, and set the distance of other nodes to infinity.
  2. Find the node u that is not in the S set and has the smallest distance, and its distance is dist[u].
  3. Add u to the S set, indicating that u has been visited.
  4. Taking u as the intermediate node, update the distance to its adjacent node v. dist[v] = min(dist[v], dist[u] + weight(u, v)).
  5. Repeat steps 2, 3 and 4 until S contains all nodes.
  6. Output the shortest path and distance of each node.

Dijkstra's algorithm uses an array dist to record the shortest path length of each node, and uses the small root heap priority queue to find the node with the smallest distance. The time and space complexities are O(nlogn) and O(n) respectively.

algorithm practice

import java.util.*;

public class DijkstraAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int[] dijkstra(int[][] graph, int start) {
    
    
        int n = graph.length;
        int[] dist = new int[n];
        boolean[] visited = new boolean[n];
        Arrays.fill(dist, INF);
        dist[start] = 0;

        for (int i = 0; i < n; i++) {
    
    
            int u = -1;
            for (int j = 0; j < n; j++) {
    
    
                if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
    
    
                    u = j;
                }
            }
            visited[u] = true;
            for (int v = 0; v < n; v++) {
    
    
                if (!visited[v] && graph[u][v] != INF && dist[u] + graph[u][v] < dist[v]) {
    
    
                    dist[v] = dist[u] + graph[u][v];
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, 8, 5},
                {
    
    INF, 3, 0, INF, 7},
                {
    
    6, 8, INF, 0, 9},
                {
    
    INF, 5, 7, 9, 0}
        };
        int start = 0;
        int[] dist = dijkstra(graph, start);
        System.out.println(Arrays.toString(dist));
    }
}

In this implementation, we first initialize the distance array dist and the visit flag array visited, and initialize all elements of the distance array to infinity (representing nodes that have not been visited). Then, we traverse all nodes from the starting point, each time selecting the smallest value in the distance array as the next node to visit. Then, we mark this node as visited, and update its distance to the not-yet-visited nodes in the distance array. Finally, we return the distance array.

The time complexity of this implementation is O(n^2), where n is the number of nodes. If priority queue is used to optimize, the time complexity can be reduced to O(m log n), where m is the number of edges.

Floyd's Algorithm

Floyd's algorithm is an algorithm for finding the shortest path between all pairs of nodes. Its basic idea is:
find out the shortest path from each node to all other nodes through recursion.
The steps of Floyd's algorithm are as follows:

  1. Initialize the dist array, dist[i][j]representing the shortest path length from node i to node j. At that timei==j , dist[i][j] = 0; when there is a direct path between node i and node j, dist[i][j]it is the path length; otherwise dist[i][j] = ∞.
  2. Traversing each intermediate node k, update the dist array:
    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    this means that the shortest path from node i to node j may pass through node k.
  3. Repeat step 2 until all intermediate nodes have been traversed.
  4. The final result of the dist array is the shortest path length between each node pair.

The time complexity and space complexity of Floyd's algorithm are both O(n3), where n is the number of nodes.

Floyd's algorithm is suitable for solving the shortest path between any two nodes, and can solve the shortest path problem of directed graph and weighted graph.

Dijkstra's algorithm and Floyd's algorithm are both classic algorithms for solving the shortest path problem, but they have the following main differences:

  1. Applicable diagram types:
    • Dijkstra's algorithm can only be used to find the single-source shortest path of directed or undirected graphs, and cannot find the shortest path between any two points.
    • Floyd's algorithm can be used to find the shortest path between any two points in a directed or undirected graph.
  2. Shortest path type:
    • Dijkstra's algorithm finds a shortest path tree, and can only get the shortest path from a single source point to other points.
    • The Floyd algorithm calculates the shortest path between all nodes at one time, and obtains a shortest path matrix.
  3. time complexity:
    • Dijkstra's algorithm is implemented using a priority queue, and the time complexity is O(nlogn).
    • The time complexity of Floyd's algorithm is O(n3).
    • When the number of nodes in the graph is large but the number of edges is small, Dijkstra's algorithm is more efficient. When the number of nodes and edges of the graph are large, Floyd's algorithm is more efficient.
  4. Space complexity:
    • Dijkstra's algorithm requires only O(n) space.
    • Floyd's algorithm requires O(n2) space to store the shortest path matrix.
  5. Is an intermediate node required:
    • Dijkstra's algorithm only considers the shortest path from the start point to the end point when updating the shortest path, without intermediate node information.
    • Floyd's algorithm needs intermediate node information when updating the shortest path, and the shortest path can be updated only through intermediate node jumps.

algorithm practice

import java.util.*;

public class FloydAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int[][] floyd(int[][] graph) {
    
    
        int n = graph.length;
        int[][] dist = new int[n][n];
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                dist[i][j] = graph[i][j];
            }
        }
        for (int k = 0; k < n; k++) {
    
    
            for (int i = 0; i < n; i++) {
    
    
                for (int j = 0; j < n; j++) {
    
    
                    if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
    
    
                        dist[i][j] = dist[i][k] + dist[k][j];
                    }
                }
            }
        }
        return dist;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, INF, INF},
                {
    
    INF, 3, 0, 4, INF},
                {
    
    6, INF, 4, 0, 8},
                {
    
    INF, INF, INF, 8, 0}
        };
        int[][] dist = floyd(graph);
        for (int i = 0; i < dist.length; i++) {
    
    
            System.out.println(Arrays.toString(dist[i]));
        }
    }
}

In this implementation, we first copy the adjacency matrix in the graph into the distance matrix. Then, we iterate over all node pairs (i,j), trying to shorten the distance between (i,j) through node k. If the distance can be shortened by passing node k, update the (i, j) element in the distance matrix.

The time complexity of this implementation is O(n^3), where n is the number of nodes.

Guess you like

Origin blog.csdn.net/zyb18507175502/article/details/130881189