【Estructura de datos y algoritmo】Teoría de grafos y algoritmos relacionados

Introducción básica a los gráficos

Una tabla lineal está limitada a una relación entre un predecesor directo y un sucesor directo, y un árbol solo puede tener un predecesor directo, es decir, un nodo principal. Cuando necesitamos representar una relación de muchos a muchos, usamos un gráfico aquí.

Un gráfico es una estructura de datos en la que un nodo puede tener cero o más elementos adyacentes. Una conexión entre dos nodos se llama un borde. Los nodos también se pueden llamar vértices. Como se muestra en la imagen:

inserte la descripción de la imagen aquí

En términos simples, un gráfico es un conjunto que consta de un conjunto finito no vacío de vértices y aristas entre ellos. Generalmente expresado como: G(V,E), donde G representa un gráfico, V representa un conjunto de vértices y E representa un conjunto de aristas.

Luego hablamos de algunos conceptos comunes en la figura:

  1. 节点(Vertex): El elemento básico en el diagrama, usado para representar una entidad.

  2. 边(Edge): Un segmento de línea que conecta dos nodos, usado para representar la relación entre nodos.

  3. 度(Degree): El grado indica cuántas aristas contiene un vértice. En un gráfico dirigido, también se divide en grado de salida y de entrada. El grado de salida indica el número de aristas que salen del vértice, y el grado de entrada indica el número de aristas que entran en el vértice.

  4. 有向图和无向图: Si el borde tiene dirección determina si el gráfico es dirigido o no. El borde dirigido indica el punto de inicio y el punto final del borde, y los dos nodos del borde no dirigido no tienen distinción entre el punto inicial y el punto final.
    inserte la descripción de la imagen aquí

  5. 带权图和无权图: El hecho de que el borde tenga un valor de peso determina si el gráfico es un gráfico ponderado o no ponderado. El borde ponderado tiene un valor que representa el peso del borde, y el peso del borde no ponderado se considera como 1.
    inserte la descripción de la imagen aquí

  6. 路径: Una secuencia de aristas entre una serie de vértices en un gráfico se llama camino.

  7. 连通性: Un grafo es conexo si existe un camino entre dos nodos cualesquiera. De lo contrario, es un gráfico desconectado.

  8. 海量图: La cantidad de nodos y aristas es enorme, mucho más allá del tamaño de la memoria de la computadora, y requiere algoritmos especiales y estructuras de almacenamiento para procesar gráficos.

representación de gráfico

Hay dos formas de representar gráficos:

  • Representación de matriz bidimensional (matriz de adyacencia)
  • Representación de lista enlazada (lista de adyacencia)

matriz de adyacencia

La matriz de adyacencia es una estructura de almacenamiento de gráficos, que utiliza una matriz bidimensional para representar los bordes entre los nodos en el gráfico.

inserte la descripción de la imagen aquí
Específicamente:

  1. El número de filas y columnas de la matriz de adyacencia es el número n de nodos en el gráfico.
  2. Cada elemento de la matriz representa una arista. Si hay una arista entre el nodo i y el nodo j, el elemento de la fila i y la columna j de la matriz es 1, de lo contrario es 0.
  3. Para gráficos ponderados, los elementos de la matriz son los pesos correspondientes, no 1.
  4. Los elementos de la diagonal principal de la matriz son todos 0, porque el nodo no estará conectado consigo mismo.
  5. La matriz de adyacencia es adecuada para representar grafos densos, y la complejidad del espacio es O(n2). Si el gráfico es escaso, la utilización del espacio es relativamente baja.

Las ventajas de una matriz de adyacencia son:

  1. Se puede determinar en tiempo O(1) si hay un borde entre dos nodos cualesquiera.
  2. Es conveniente implementar algunos algoritmos, como la búsqueda primero en amplitud, la búsqueda primero en profundidad, etc.

Las desventajas de una matriz de adyacencia son:

  1. La complejidad del espacio es alta, y si el gráfico es escaso, se desperdiciará espacio.
  2. Es difícil representar un gráfico dinámico, si los bordes se agregan y eliminan con frecuencia, la matriz de adyacencia debe reconstruirse con frecuencia.

lista de adyacencia

La lista de adyacencia es otra estructura de almacenamiento del gráfico, que utiliza una lista vinculada para almacenar los nodos adyacentes de cada nodo.

inserte la descripción de la imagen aquí

Específicamente:

  1. Las listas de adyacencia se componen de matrices y listas enlazadas. Cada elemento de la matriz es una lista enlazada, que representa la lista de nodos adyacentes del nodo correspondiente.
  2. El índice de la matriz es el número de serie del nodo y el tamaño de la matriz es el número de nodos n.
  3. Si el nodo i tiene un borde con el nodo j, agregue j a la lista vinculada correspondiente a i.
  4. Para los gráficos ponderados, los elementos de cada nodo en la lista enlazada ya no solo almacenan el número de serie del nodo adyacente, sino que almacenan una estructura, que incluye el número de serie del nodo adyacente y el peso correspondiente.
  5. La lista de adyacencia es adecuada para gráficos dispersos y la complejidad del espacio es O(n+e), donde e es el número de aristas. Para gráficos densos, la tasa de utilización del espacio será mayor.

Las ventajas de la lista de adyacencia son:
6. La complejidad del espacio es baja y es adecuada para representar gráficos dispersos.
7. Es fácil realizar la operación de agregar y eliminar gráficos dinámicos.

Las desventajas de la lista de adyacencia son:
8. Es difícil juzgar si hay un borde entre dos nodos cualesquiera en el tiempo O(1), y es necesario atravesar la lista enlazada.
9. No es conveniente implementar algunos algoritmos, como la búsqueda primero en amplitud y la búsqueda primero en profundidad.

Recorrido primero en profundidad (DFS) de gráficos

El llamado recorrido de grafos es el acceso a los nodos. Un gráfico tiene tantos nodos, cómo atravesar estos nodos requiere una estrategia específica, generalmente hay dos estrategias de acceso:

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

descripción general

Recorrido primero en profundidad, comenzando desde el nodo de acceso inicial, el nodo de acceso inicial puede tener múltiples nodos adyacentes, la estrategia del recorrido primero en profundidad es visitar primero el primer nodo adyacente y luego usar el nodo adyacente visitado como el nodo inicial para visitar su primer nodo adyacente.

Se puede entender de la siguiente manera: cada vez que visite el nodo actual, primero visite el primer nodo adyacente del nodo actual. Podemos ver que tal estrategia de acceso es profundizar verticalmente en lugar de acceder horizontalmente a todos los nodos adyacentes de un nodo. Obviamente, la búsqueda primero en profundidad es un proceso recursivo

inserte la descripción de la imagen aquí

En general: La búsqueda primero en profundidad (Depth-First Search) es un algoritmo de recorrido de gráfico. Comienza desde un cierto nodo y busca lo más profundamente posible hasta que se recorren todos los nodos en la ruta actual. Luego retroceda, y continúe buscando el siguiente camino lo más profundo posible .

La aplicación principal de la búsqueda primero en profundidad esComprobación de conectividad de grafos, clasificación topológica, resolución del número de cortes y resolución del número máximo coincidente de grafos bipartitos, etc.

Pasos de implementación

El proceso de búsqueda en profundidad se puede implementar de forma recursiva. Los pasos principales son los siguientes:

  1. A partir de un nodo v en el gráfico de destino, visite el nodo.
  2. Si no se ha visitado el nodo vecino w de v, visite recursivamente w.
  3. Si se ha visitado w, regrese al nodo v y visite otro nodo adyacente no visitado de v.
  4. Si se han visitado todos los nodos adyacentes de v, retroceda al nodo anterior de v.
  5. Repita los pasos 3 y 4 hasta que se visiten todos los nodos.

Dado que se usa la recursividad, definitivamente podemos usar la idea de la pila para entender:

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

Código

inserte la descripción de la imagen aquí

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);
	}
	

}

Aviso:

  • En nuestro método getFirstNeighbor, la j aquí debe comenzar desde 0, y algunas personas tienden a comenzar con index+1, lo que no tiene en cuenta la situación de que el recorrido no comienza desde A.
  • La razón para atravesar nuestra lista de nodos es que puede ser un grafo desconectado, y atravesar desde un nodo puede no atravesar todos los nodos del grafo completo para
    inserte la descripción de la imagen aquí

Recorrido primero en amplitud (BFS) de gráficos

descripción general

Breadth-First Search es un algoritmo de recorrido de gráfico. A partir de un determinado nodo, primero visita todos los nodos adyacentes al nodo, luego visita los nodos adyacentes del nodo adyacente, y así sucesivamente, hasta que se recorren todos los nodos.

La búsqueda primero en amplitud se expande capa por capa como ondas en el agua:
inserte la descripción de la imagen aquí

En comparación con la búsqueda primero en profundidad, las características de la búsqueda primero en amplitud son:

  1. Primero se visitarán los nodos más cercanos al nodo inicial. La búsqueda primero en profundidad buscará lo más profundo posible, posiblemente más lejos del punto de partida.
  2. Implementado usando una cola, la complejidad del espacio es alta. La búsqueda primero en profundidad utiliza una pila recursiva con una complejidad de espacio baja.

La búsqueda en amplitud se utiliza principalmente paraEl problema del camino más corto en grafos, clasificación topológicaesperar.

Pasos de implementación

El proceso de búsqueda primero en amplitud se puede implementar con una cola. Los pasos principales son los siguientes:

  1. A partir de un cierto nodo v en el gráfico, visite el nodo y póngalo en cola.
  2. Saque el nodo principal del equipo, visite todos los nodos adyacentes no visitados de este nodo y ponga en cola los nodos adyacentes.
  3. Repita el paso 2 hasta que la cola esté vacía.
  4. Si hay nodos no visitados en el gráfico, comience desde uno de los nodos no visitados y repita los pasos 1-3.

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

Código

inserte la descripción de la imagen aquí

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();
	}


}

Resumen de códigos comunes para gráficos

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++;
	}
}

Algoritmo de árbol de expansión mínimo

El árbol de expansión mínimo es un concepto muy importante en la teoría de grafos.Se refiere a un árbol que conecta todos los nodos en el gráfico, y la suma de los pesos de todos los bordes de este árbol es la más pequeña.

Un árbol de expansión mínimo tiene varias propiedades importantes:

  1. Contiene todos los nodos del gráfico, no hay puntos aislados.
  2. Es un árbol, sin anillos.
  3. Su suma de pesos es la más pequeña.

inserte la descripción de la imagen aquí

Los algoritmos de árbol de expansión mínimos comunes son:
4. Prim算法: Comience desde un nodo y agregue continuamente nuevos nodos y bordes al árbol de expansión hasta que se incluyan todos los nodos. Cada nuevo borde agregado es el borde más corto de un nodo en el árbol.
5. Kruskal算法: Seleccione el borde de acuerdo con el peso del borde de pequeño a grande. Siempre que este borde no forme un anillo, se agregará al árbol de expansión mínimo.
6. Dijkstra算法: Use el algoritmo de ruta más corta de Dijkstra para encontrar la ruta más corta desde cada nodo a todos los demás nodos, y el árbol de expansión mínimo se forma con estas rutas más cortas.

El árbol de expansión mínimo tiene muchas aplicaciones prácticas, como conectividad de red, cableado de circuitos, etc. Proporciona una solución eficiente a estos problemas prácticos.

En resumen, el árbol de expansión mínimo es un concepto muy clásico e importante en la teoría de grafos, y los algoritmos relacionados también son importantes, y vale la pena comprenderlos y dominarlos.

Demostración de animación de algoritmo de árbol de expansión mínimo (Kruskal (Kruskal) y Prim (Prim))

Algoritmo de Prim

El algoritmo de Prim es uno de los algoritmos clásicos del árbol de expansión mínimo. Su idea básica es:

  • A partir de cualquier nodo del gráfico, agregue aristas y nodos paso a paso para formar un árbol de expansión mínimo. Cada nueva arista agregada debe conectar nodos en el árbol y nodos que no están en el árbol, y esta nueva arista debe ser la que tenga el menor peso entre todas las aristas candidatas.

Los pasos del algoritmo de Prim son los siguientes:

  1. Seleccione cualquier nodo en el gráfico como el nodo inicial y marque ese nodo como visitado.
  2. Encuentre un borde con el menor peso entre todos los nodos no visitados conectados a los nodos visitados. Los nodos no visitados conectados por este borde se marcan como visitados.
  3. Repita el paso 2 hasta que se visiten todos los nodos.
  4. El conjunto de aristas formado es el árbol de expansión mínimo.

O puede consultar el siguiente video que lo explica muy claramente:

Diagrama de algoritmo:
inserte la descripción de la imagen aquí

La complejidad temporal del algoritmo de Prim es O(n2), y puede reducirse a O(nlogn) si se implementa con una cola de prioridad.

El algoritmo de Prim solo es aplicable a gráficos conectados no dirigidos ponderados. Si el gráfico es dirigido o desconectado, el algoritmo de Prim no puede obtener el árbol de expansión mínimo.

práctica de algoritmos

Código:

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);
    }
}

En esta implementación, primero inicializamos la matriz de distancia dist y la matriz de bandera de visita visitada, e inicializamos todos los elementos de la matriz de distancia hasta el infinito (que representan los nodos que no han sido visitados).

Luego, recorremos todos los nodos comenzando desde el nodo 0, seleccionando cada vez el valor más pequeño en la matriz de distancia como el próximo nodo a visitar. Luego, marcamos este nodo como visitado y actualizamos su distancia a los nodos aún no visitados en la matriz de distancia. Finalmente, devolvemos la suma de todos los pesos de los bordes como el peso mínimo del árbol de expansión.

La complejidad temporal de esta implementación es O(n^2), donde n es el número de nodos.

Algoritmo de Kruskal

El algoritmo de Kruskal es otro algoritmo clásico para el árbol de expansión mínimo. Su idea básica es:
ordenar todos los bordes en el gráfico de acuerdo a sus pesos de menor a mayor. Seleccione el borde con el peso más pequeño y agréguelo al árbol de expansión mínimo siempre que el borde no forme un ciclo. Repita este paso hasta que el árbol de expansión mínimo contenga todos los nodos del gráfico.

Los pasos del algoritmo de Kruskal son los siguientes:

  1. Ordene todos los bordes del gráfico en orden ascendente de peso.
  2. Seleccione el borde con el peso más pequeño para determinar si se forma un anillo. Si no forma un ciclo, se agrega al árbol de expansión mínimo.
  3. Repita el paso 2 hasta que el árbol de expansión mínimo contenga todos los vértices del gráfico.
  4. Salida de un árbol de expansión mínimo.

La complejidad temporal del algoritmo de Kruskal es O(ElogE), donde E es el número de aristas en el gráfico.

La implementación del algoritmo de Kruskal necesita usar el conjunto de búsqueda de unión para juzgar si el borde seleccionado formará un anillo . La verificación de unión puede juzgar si dos elementos pertenecen al mismo conjunto en el tiempo O (1), que es la clave para realizar el algoritmo de Kruskal.

En comparación con el algoritmo de Prim, el algoritmo de Kruskal es adecuado para gráficos dispersos con más aristas, porque su complejidad temporal no depende del número de nodos, sino solo de la cantidad de aristas. Pero el algoritmo de Kruskal necesita ordenar todos los bordes por adelantado, lo que aumenta la complejidad del espacio.

Ilustración:
inserte la descripción de la imagen aquí

y buscar

El conjunto de verificación de unión es una estructura de datos de tipo árbol, que se utiliza para tratar los problemas de combinación y consulta de algunos conjuntos disjuntos .
Soporta tres operaciones:

inserte la descripción de la imagen aquí

  1. inicializar init
  2. union(x, y): combine la colección donde se encuentran el elemento x y el elemento y.
  3. find(x): Encuentre el representante del conjunto donde se encuentra el elemento x, y el representante del conjunto es el primer elemento agregado al conjunto.

La búsqueda de unión implementa una especie de conectividad dinámica. Al principio, cada elemento forma un conjunto por sí mismo, y los conjuntos se fusionan continuamente a través de la operación de unión, y finalmente se forman varios conjuntos grandes disjuntos.

La aplicación típica de la búsqueda de unión es resolver el problema de consulta fuera de línea en la teoría de grafos, como consultar si dos nodos están en el mismo gráfico conectado en un gráfico no dirigido.

Hay dos formas comunes de implementar el control de unión:

  1. Búsqueda rápida: al usar una estructura de árbol, la operación de búsqueda debe atravesar el nodo raíz y la complejidad del tiempo es O (n).
  2. Combinación rápida con compresión de rutas: durante el proceso transversal de la operación de búsqueda, apunte el nodo directamente al nodo raíz para lograr la compresión de rutas y reducir la profundidad del árbol. Después del balanceo, la complejidad del tiempo puede llegar a O(1).

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

práctica de algoritmos

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);
    }
}

En esta implementación, primero almacenamos todos los bordes del gráfico en una lista y los ordenamos en orden ascendente de peso. Luego, inicializamos una búsqueda de unión, inicializando el padre de cada nodo a sí mismo.

A continuación, recorremos la lista de aristas ordenadas.Para cada arista, si sus dos extremos no están en el mismo bloque conectado, fusionarlos en el mismo bloque conectado y agregar el peso de esta arista al peso del árbol de expansión mínimo.

La complejidad temporal de esta implementación es O(m log m), donde m es el número de aristas.

algoritmo de ruta más corta

Demostración de animación de algoritmo de distancia más corta (ruta más corta) de teoría de grafos - Dijkstra (Dijkstra) y Floyd (Floyd)

Algoritmo de Dijkstra

El algoritmo de Dijkstra es un algoritmo de ruta más corta típico, que se utiliza para calcular la ruta más corta de un nodo a otros nodos. Su principal característica es la de expandirse desde el punto de partida hasta la capa exterior (idea de búsqueda en amplitud) hasta llegar al final.

El algoritmo de Dijkstra es un algoritmo para encontrar el camino más corto de una sola fuente. Su idea básica es:

  1. Seleccione un nodo como nodo inicial y calcule la ruta más corta desde el nodo inicial a otros nodos.
  2. Atraviese todos los nodos accesibles desde el nodo inicial y actualice la ruta más corta. Seleccione el siguiente nodo para continuar atravesando hasta que se atraviesen todos los nodos.
  3. Repita los pasos anteriores hasta que finalmente obtenga la ruta más corta desde el nodo inicial a todos los nodos.

Los pasos del algoritmo de Dijkstra son los siguientes:

  1. Seleccione la fuente del nodo inicial, establezca su distancia en 0 y establezca la distancia de otros nodos en infinito.
  2. Encuentre el nodo u que no está en el conjunto S y tiene la distancia más pequeña, y su distancia es dist[u].
  3. Agregue u al conjunto S, lo que indica que u ha sido visitado.
  4. Tomando u como el nodo intermedio, actualice la distancia a su nodo adyacente v. dist[v] = min(dist[v], dist[u] + peso(u, v)).
  5. Repita los pasos 2, 3 y 4 hasta que S contenga todos los nodos.
  6. Muestra la ruta más corta y la distancia de cada nodo.

El algoritmo de Dijkstra usa una matriz dist para registrar la longitud de la ruta más corta de cada nodo y usa la cola de prioridad del montón raíz pequeño para encontrar el nodo con la distancia más pequeña. Las complejidades de tiempo y espacio son O(nlogn) y O(n) respectivamente.

práctica de algoritmos

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));
    }
}

En esta implementación, primero inicializamos la matriz de distancia dist y la matriz de bandera de visita visitada, e inicializamos todos los elementos de la matriz de distancia hasta el infinito (que representan los nodos que no han sido visitados). Luego, recorremos todos los nodos desde el punto de inicio, seleccionando cada vez el valor más pequeño en la matriz de distancia como el siguiente nodo a visitar. Luego, marcamos este nodo como visitado y actualizamos su distancia a los nodos aún no visitados en la matriz de distancia. Finalmente, devolvemos la matriz de distancia.

La complejidad temporal de esta implementación es O(n^2), donde n es el número de nodos. Si se utiliza la cola de prioridad para optimizar, la complejidad del tiempo se puede reducir a O (m log n), donde m es el número de aristas.

Algoritmo de Floyd

El algoritmo de Floyd es un algoritmo para encontrar el camino más corto entre todos los pares de nodos. Su idea básica es:
encontrar el camino más corto desde cada nodo a todos los demás nodos a través de la recursividad.
Los pasos del algoritmo de Floyd son los siguientes:

  1. Inicialice la matriz dist, dist[i][j]que representa la longitud de ruta más corta desde el nodo i hasta el nodo j. En ese momentoi==j , dist[i][j] = 0; cuando hay un camino directo entre el nodo i y el nodo j, dist[i][j]es la longitud del camino; de lo contrario dist[i][j] = ∞.
  2. Atravesando cada nodo intermedio k, actualice la matriz dist:
    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    esto significa que la ruta más corta desde el nodo i al nodo j puede pasar por el nodo k.
  3. Repita el paso 2 hasta que se hayan atravesado todos los nodos intermedios.
  4. El resultado final de la matriz dist es la longitud de ruta más corta entre cada par de nodos.

La complejidad temporal y espacial del algoritmo de Floyd son O(n3), donde n es el número de nodos.

El algoritmo de Floyd es adecuado para resolver el camino más corto entre dos nodos cualesquiera y puede resolver el problema del camino más corto del gráfico dirigido y el gráfico ponderado.

El algoritmo de Dijkstra y el algoritmo de Floyd son algoritmos clásicos para resolver el problema del camino más corto, pero tienen las siguientes diferencias principales:

  1. Tipos de diagramas aplicables:
    • El algoritmo de Dijkstra solo se puede usar para encontrar la ruta más corta de una sola fuente de gráficos dirigidos o no dirigidos, y no puede encontrar la ruta más corta entre dos puntos.
    • El algoritmo de Floyd se puede utilizar para encontrar el camino más corto entre dos puntos en un gráfico dirigido o no dirigido.
  2. Tipo de ruta más corta:
    • El algoritmo de Dijkstra encuentra el árbol de la ruta más corta y solo puede obtener la ruta más corta desde un único punto de origen a otros puntos.
    • El algoritmo de Floyd calcula la ruta más corta entre todos los nodos a la vez y obtiene una matriz de ruta más corta.
  3. complejidad del tiempo:
    • El algoritmo de Dijkstra se implementa utilizando una cola de prioridad y la complejidad de tiempo es O (nlogn).
    • La complejidad temporal del algoritmo de Floyd es O(n3).
    • Cuando el número de nodos en el gráfico es grande pero el número de aristas es pequeño, el algoritmo de Dijkstra es más eficiente. Cuando el número de nodos y bordes del gráfico es grande, el algoritmo de Floyd es más eficiente.
  4. Complejidad del espacio:
    • El algoritmo de Dijkstra requiere solo espacio O(n).
    • El algoritmo de Floyd requiere espacio O(n2) para almacenar la matriz de ruta más corta.
  5. Se requiere un nodo intermedio:
    • El algoritmo de Dijkstra solo considera la ruta más corta desde el punto de inicio hasta el punto final al actualizar la ruta más corta, sin información de nodo intermedio.
    • El algoritmo de Floyd necesita información del nodo intermedio al actualizar la ruta más corta, y la ruta más corta se puede actualizar solo a través de saltos de nodos intermedios.

práctica de algoritmos

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]));
        }
    }
}

En esta implementación, primero copiamos la matriz de adyacencia en el gráfico en la matriz de distancia. Luego, iteramos sobre todos los pares de nodos (i, j), tratando de acortar la distancia entre (i, j) y el nodo k. Si la distancia se puede acortar pasando el nodo k, actualice el elemento (i, j) en la matriz de distancia.

La complejidad temporal de esta implementación es O(n^3), donde n es el número de nodos.

Supongo que te gusta

Origin blog.csdn.net/zyb18507175502/article/details/130881189
Recomendado
Clasificación