Está en la página 1de 19

Algoritmo Prim

1. Inicio: Elige un nodo inicial arbitrario y agrégalo al árbol de expansión


mínimo.
2. Repetición: En cada paso, elige la arista más pequeña que conecta un
vértice ya incluido en el árbol con uno que aún no está incluido. Agrega este
vértice y la arista al árbol.
3. Criterio de selección: En cada paso, elige la arista más pequeña que
conecta un vértice incluido en el árbol con uno que aún no está incluido.
Esto garantiza que siempre se escoja la arista más pequeña para expandir el
árbol.
4. Finalización: El algoritmo se detiene cuando se han incluido todos los
vértices en el árbol.

En resumen, el algoritmo comienza con un solo vértice y agrega repetidamente el


vértice más cercano (a través de la arista más corta) hasta que todos los vértices
estén conectados.

Entrada: Grafo no dirigido con pesos

1. Inicializar un árbol vacío y un conjunto para almacenar vértices ya incluidos.

2. Elegir un vértice inicial arbitrario.

3. Marcar este vértice como incluido.

4. Repetir hasta que todos los vértices estén incluidos:

- Encontrar la arista más pequeña que conecta un vértice incluido con uno no incluido.

- Agregar el vértice no incluido y la arista al árbol.

- Marcar el vértice agregado como incluido.

5. El árbol resultante es el árbol de expansión mínimo.

Running time O(ElogV)

La razón fundamental detrás de la complejidad O(E log V) es que para cada una de las E
aristas, puede haber un máximo de log(V) inserciones y extracciones en el min-heap. En el
peor caso, cada una de las E aristas se agrega y extrae del heap una vez, lo que resulta en
O(E log V).
from collections import defaultdict

import heapq

class Graph:

def __init__(self, vertices):

self.V = vertices

self.graph = defaultdict(list)

def add_edge(self, src, dest, weight):

self.graph[src].append((dest, weight))

self.graph[dest].append((src, weight)) # Para un grafo no dirigido

def prim_mst(self):

min_heap = [(0, 0)] # Tupla: (peso, vértice)

visited = set()

mst_weight = 0

while min_heap:

weight, vertex = heapq.heappop(min_heap)

if vertex not in visited:

visited.add(vertex)

mst_weight += weight

for neighbor, edge_weight in self.graph[vertex]:

if neighbor not in visited:

heapq.heappush(min_heap, (edge_weight, neighbor))

return mst_weight
# Creación del grafo con 5 vértices y sus aristas

g = Graph(5)

g.add_edge(0, 1, 2)

g.add_edge(0, 3, 3)

g.add_edge(1, 2, 4)

g.add_edge(1, 3, 5)

g.add_edge(1, 4, 1)

g.add_edge(2, 4, 6)

g.add_edge(3, 4, 7)

# Obtener el peso total del árbol de expansión mínimo (MST)

mst_weight = g.prim_mst()

print("El peso del árbol de expansión mínimo (MST) es:", mst_weight)

Algoritmo Kruskal

El algoritmo de Kruskal es otro algoritmo voraz (greedy) utilizado para


encontrar el árbol de expansión mínimo en un grafo no dirigido y con
pesos. Funciona de la siguiente manera:

1. Ordenar las aristas: Se comienza ordenando todas las aristas del


grafo en orden creciente según sus pesos.
2. Inicialización: Se crea un bosque (conjunto de árboles) donde cada
vértice está en un árbol separado.
3. Iteración: Para cada arista en orden ascendente de peso:
 Si la arista une dos árboles diferentes en el bosque actual (sin
formar un ciclo), se agrega al árbol de expansión mínimo.
 Si la arista une dos vértices en el mismo árbol, se descarta
para evitar la formación de ciclos.
4. Finalización: El algoritmo se detiene cuando se han considerado
todas las aristas o cuando el número de aristas seleccionadas
alcanza V-1, donde V es el número de vértices del grafo.
El algoritmo de Kruskal selecciona aristas en orden ascendente de peso y
las agrega al árbol de expansión mínimo siempre y cuando no formen
ciclos. Esto garantiza que al final se obtenga un árbol sin ciclos que
abarque todos los vértices con el mínimo peso posible.

Una de las estructuras de datos clave utilizadas en este algoritmo es la


estructura de conjuntos disjuntos (Union-Find), que se emplea para
verificar y evitar la formación de ciclos al unir vértices.

En resumen, Kruskal busca las aristas de menor peso y las agrega al árbol
de expansión mínimo siempre y cuando no generen ciclos. Al final, se
obtiene un árbol que conecta todos los vértices con el menor peso
posible.
Ordenamiento de las aristas: O(E log E) o O(E log V) - El costo de ordenar las aristas por
peso. Si las aristas están ordenadas por sus pesos, esto puede ser O(E log E), pero si E es
considerablemente mayor que V, se puede aproximar a O(E log V) debido a la relación
entre E y V.
class Graph:

def __init__(self, vertices):

self.V = vertices

self.graph = []

def add_edge(self, src, dest, weight):

self.graph.append((src, dest, weight))

def find_parent(self, parent, i):

if parent[i] == i:

return i

return self.find_parent(parent, parent[i])

def union(self, parent, rank, x, y):

x_root = self.find_parent(parent, x)

y_root = self.find_parent(parent, y)
if rank[x_root] < rank[y_root]:

parent[x_root] = y_root

elif rank[x_root] > rank[y_root]:

parent[y_root] = x_root

else:

parent[y_root] = x_root

rank[x_root] += 1

def kruskal_mst(self):

result = []

self.graph = sorted(self.graph, key=lambda item: item[2])

parent = []

rank = []

for node in range(self.V):

parent.append(node)

rank.append(0)

i=0

e=0

while e < self.V - 1 and i < len(self.graph):

src, dest, weight = self.graph[i]

i += 1

x = self.find_parent(parent, src)

y = self.find_parent(parent, dest)

if x != y:
e += 1

result.append((src, dest, weight))

self.union(parent, rank, x, y)

total_weight = sum(weight for _, _, weight in result)

return total_weight

# Creación del grafo con 6 vértices y sus aristas

g = Graph(6)

g.add_edge(0, 1, 4)

g.add_edge(0, 2, 3)

g.add_edge(1, 2, 1)

g.add_edge(1, 3, 2)

g.add_edge(1, 4, 3)

g.add_edge(2, 3, 4)

g.add_edge(3, 4, 5)

g.add_edge(4, 5, 6)

# Obtener el peso total del árbol de expansión mínimo (MST)

mst_weight = g.kruskal_mst()

print("El peso del árbol de expansión mínimo (MST) es:", mst_weight)

Algoritmo de multiplicación en cadena de matrices

Claro, el problema de la multiplicación de matrices en cadena implica determinar el


orden óptimo de multiplicación cuando se tienen múltiples matrices y se quiere
minimizar el número total de operaciones realizadas.

Imagina que tienes matrices A, B, C y D. El número de operaciones para multiplicar


estas matrices depende del orden en el que se realicen las multiplicaciones. Por
ejemplo, si multiplicamos primero A por B, y luego el resultado por C y finalmente
por D, el número total de operaciones puede variar si cambiamos el orden de
multiplicación.

El algoritmo de multiplicación en cadena de matrices utiliza programación


dinámica para resolver este problema. Funciona de la siguiente manera:

1. Definir la estructura de datos: Se tiene una secuencia de matrices M1,


M2, ..., Mn. Cada matriz tiene dimensiones diferentes, por ejemplo, M1
puede ser 10x30, M2 puede ser 30x5, y así sucesivamente.
2. Construir una tabla de costos: Se crea una tabla donde se almacenarán los
costos mínimos de multiplicación para cada subcadena de matrices. La tabla
es cuadrada y su tamaño es n por n, siendo n el número de matrices.
3. Llenar la tabla utilizando programación dinámica: Se calculan los costos
mínimos de multiplicación para subcadenas de matrices de longitud
creciente. Esto se hace iterativamente para todas las longitudes posibles de
subcadenas de matrices, desde cadenas de longitud 2 hasta cadenas de
longitud n.
4. Recuperar el orden óptimo de multiplicación: Además de los costos
mínimos, se mantiene un registro del punto de división óptimo para cada
subcadena de matrices, lo que permite reconstruir el orden óptimo de
multiplicación.
5. Obtener el número mínimo de operaciones: Al finalizar el proceso, se
obtiene el número mínimo de operaciones necesario para multiplicar todas
las matrices, así como el orden óptimo en el que deben multiplicarse para
alcanzar ese número mínimo de operaciones.

El algoritmo de multiplicación en cadena de matrices utiliza la propiedad asociativa


de la multiplicación de matrices para explorar y encontrar el orden óptimo de
multiplicación que minimiza el número total de operaciones requeridas. Esto se
logra a través de la técnica de programación dinámica, evitando recalcular
resultados y optimizando el proceso de toma de decisiones sobre el orden de
multiplicación de las matrices.

El algoritmo de multiplicación en cadena de matrices utilizando programación


dinámica tiene una complejidad temporal de O(n^3), donde 'n' es el número de
matrices que se están multiplicando.

Aquí está el análisis de la complejidad:


1. Construcción de la tabla de costos: Se construye una tabla de tamaño n
por n para almacenar los costos mínimos de multiplicación para todas las
combinaciones de subcadenas de matrices. Esto implica dos bucles anidados
para recorrer todas las combinaciones de subcadenas de matrices, lo que da
una complejidad de O(n^2).
2. Llenado de la tabla utilizando programación dinámica: En cada celda de
la tabla, se calcula el costo mínimo de multiplicación considerando todas las
posibles divisiones de la subcadena en subcadenas más pequeñas. Este
cálculo implica otro bucle sobre la longitud de las subcadenas, lo que añade
una complejidad de O(n) por cada celda.

En consecuencia, la complejidad total es O(n^2) para la construcción de la tabla,


multiplicada por O(n) para el cálculo en cada celda, lo que resulta en una
complejidad temporal total de O(n^3).

Esta complejidad hace que el algoritmo de multiplicación en cadena de matrices


utilizando programación dinámica sea eficiente, incluso para un número
considerable de matrices. Sin embargo, a medida que el número de matrices crece,
el tiempo de ejecución puede volverse significativo, por lo que este algoritmo
resulta particularmente útil para matrices grandes o secuencias extensas de
matrices a multiplicar.

import sys

def matrix_chain_order(p):

n = len(p) - 1 # Número de matrices

m = [[0 for _ in range(n)] for _ in range(n)] # Almacenar el número mínimo de operaciones

s = [[0 for _ in range(n)] for _ in range(n)] # Almacenar la ubicación del split óptimo

for l in range(2, n + 1):

for i in range(n - l + 1):

j=i+l-1

m[i][j] = sys.maxsize # Inicializar con un valor grande

for k in range(i, j):


cost = m[i][k] + m[k + 1][j] + p[i] * p[k + 1] * p[j + 1]

if cost < m[i][j]:

m[i][j] = cost

s[i][j] = k

return m, s

def print_optimal_parens(s, i, j):

if i == j:

print(f"A{i}", end="")

else:

print("(", end="")

print_optimal_parens(s, i, s[i][j])

print_optimal_parens(s, s[i][j] + 1, j)

print(")", end="")

# Matrices: A1 de dimensiones 10x30, A2 de dimensiones 30x5, A3 de dimensiones 5x60, A4 de


dimensiones 60x10, A5 de dimensiones 10x80

matrix_dimensions = [10, 30, 5, 60, 10, 80]

m, s = matrix_chain_order(matrix_dimensions)

print("Número mínimo de operaciones necesarias:", m[0][-1])

print("Secuencia óptima de multiplicación:", end=" ")

print_optimal_parens(s, 0, len(matrix_dimensions) - 2)

Algoritmo de decomposición A=LU


La descomposición LU es una técnica que descompone una matriz A en
dos matrices: una matriz triangular inferior (L) y una matriz triangular
superior (U). Esta descomposición se denota como A = LU.

La matriz A se puede descomponer en L y U de la siguiente manera:

 Matriz triangular inferior (L): Es una matriz con unos en la


diagonal principal y valores arbitrarios por encima de la diagonal.
Los elementos por encima de la diagonal son los multiplicadores
utilizados durante la eliminación gaussiana para llevar la matriz
original a una forma triangular superior. La diagonal principal de L
siempre consiste en unos.
 Matriz triangular superior (U): Es una matriz que contiene los
valores resultantes después de aplicar la eliminación gaussiana a la
matriz original. Todos los elementos por debajo de la diagonal son
cero.

El algoritmo de descomposición LU se basa en el método de eliminación


gaussiana para resolver sistemas de ecuaciones lineales. Durante la
eliminación gaussiana, se realizan operaciones en la matriz original para
llevarla a una forma triangular superior. Al hacerlo, se registran los
multiplicadores utilizados en la matriz L, y los resultados finales de la
eliminación forman la matriz U.

El proceso para obtener L y U generalmente implica pasos como:

1. Inicialización de L como una matriz identidad y U como una copia


de la matriz A original.
2. Para cada columna 'j' desde la primera hasta la penúltima:
 Se usa el elemento diagonal en la fila 'j' como pivote.
 Se realiza la eliminación gaussiana para hacer cero los
elementos debajo del pivote en la columna 'j'. Los
multiplicadores utilizados en este proceso se almacenan en la
matriz L.
 Los resultados finales de la eliminación forman la matriz U.
3. La matriz L resultante tendrá los multiplicadores correctos debajo
de la diagonal principal, y la matriz U contendrá los resultados
finales de la eliminación.

La descomposición LU es útil para resolver sistemas de ecuaciones


lineales y también facilita la inversión de matrices, ya que A = LU permite
resolver sistemas Ax = b con mayor eficiencia al tener matrices triangular
superior e inferior.
El tiempo de ejecución del algoritmo de descomposición LU puede variar
según la implementación específica y las características de la matriz de
entrada. Sin embargo, en términos generales, la descomposición LU tiene
una complejidad temporal aproximada de O(n^3), donde 'n' es el tamaño
de la matriz cuadrada 'n x n' que se está descomponiendo.

Aquí está el análisis de la complejidad:

1. Operaciones para calcular la matriz U: Para llevar la matriz


original a una forma triangular superior, se realizan operaciones en
las filas para eliminar los elementos debajo de la diagonal principal.
Esto implica aproximadamente O(n^2) operaciones.
2. Operaciones para calcular la matriz L: La matriz L se calcula
almacenando los multiplicadores utilizados durante la eliminación
gaussiana para cada columna. Se realizan alrededor de O(n^2)
operaciones para calcular estos multiplicadores.
3. Construcción de L y U: Combinando los pasos anteriores, la
construcción de ambas matrices implica alrededor de O(n^2)
operaciones.

Por lo tanto, la complejidad total es aproximadamente O(n^3). Este valor


puede variar ligeramente dependiendo de la eficiencia de la
implementación y de las propiedades específicas de la matriz, como la
presencia de ceros estratégicos que puedan reducir el número de
operaciones. Sin embargo, en términos generales, la descomposición LU
tiene una complejidad cúbica y puede ser costosa computacionalmente
para matrices grandes.
Aplicación de la descomposición LU para encontrar inversa y encontrar solución de manera mas
eficiente.

La descomposición LU es una herramienta poderosa que facilita la


resolución de sistemas de ecuaciones lineales y la inversión de matrices
de manera más eficiente. Veamos cómo se utiliza para encontrar la
inversa y resolver sistemas de ecuaciones lineales.

Encontrar la inversa de una matriz utilizando la


descomposición LU:
Una vez que se ha realizado la descomposición LU de una matriz A = LU,
podemos utilizar esta descomposición para encontrar la inversa de A de
manera más eficiente. Si tenemos:

A=LU

Para encontrar la inversa de A, resolvemos dos sistemas de ecuaciones


lineales:

1. Encontrar la inversa de L (L_inv): Resolvemos L⋅Linv=I, donde I es


la matriz identidad. Esto se hace utilizando sustitución hacia
adelante. Una vez que hemos encontrado Linv, obtenemos la matriz
U.
2. Encontrar la inversa de U (U_inv): Resolvemos U⋅Uinv=Linv,
utilizando sustitución hacia atrás para resolver este sistema de
ecuaciones lineales.

Finalmente, la inversa de la matriz A se obtiene como:

A−1=Uinv⋅Linv

Resolver sistemas de ecuaciones lineales utilizando la


descomposición LU:
Una vez que tenemos la descomposición LU de la matriz A = LU, resolver
un sistema de ecuaciones lineales Ax = b se vuelve más eficiente.
Dado que A = LU, podemos expresar Ax = b como L(Ux) = b. Entonces,
definimos Ux como un nuevo vector y resolvemos dos sistemas de
ecuaciones lineales:

1. Resolver Ly = b usando sustitución hacia adelante: Encontramos


el vector 'y' resolviendo el sistema triangular inferior Ly = b
utilizando sustitución hacia adelante.
2. Resolver Ux = y usando sustitución hacia atrás: Una vez que
tenemos 'y', resolvemos el sistema triangular superior Ux = y
utilizando sustitución hacia atrás para obtener el vector 'x'.

Esta técnica es más eficiente que resolver directamente el sistema de


ecuaciones lineales utilizando métodos como la eliminación gaussiana,
especialmente si se necesita resolver varios sistemas de ecuaciones
lineales con la misma matriz A pero diferentes vectores 'b'.

En resumen, la descomposición LU no solo es útil para la resolución


eficiente de sistemas de ecuaciones lineales, sino que también facilita el
cálculo de la inversa de una matriz, lo que puede ser crucial en muchas
aplicaciones matemáticas y científicas.

sistema UX=b
Algoritmo de solución sistema lineal LX=b
import numpy as np
# Función para resolver Lx = b usando sustitución hacia adelante

def forward_substitution(L, b):

n = len(b)

x = np.zeros(n)

for i in range(n): # Comenzar desde la primera fila y avanzar hacia abajo

x[i] = b[i]

for j in range(i): # Utilizar los valores ya calculados de x

x[i] -= L[i][j] * x[j]

x[i] /= L[i][i]

return x

# Ejemplo de una matriz triangular inferior L y un vector b

L = np.array([[2, 0, 0],

[5, 3, 0],

[1, 8, 4]])

b = np.array([4, 6, 2])

# Resolver Lx = b usando sustitución hacia adelante

solution = forward_substitution(L, b)

print("La solución x es:", solution)


Algoritmo usual de multiplicación de matrices

import numpy as np
# Función para la multiplicación de matrices

def matrix_multiplication(A, B):

rows_A, cols_A = A.shape

rows_B, cols_B = B.shape

# Verificar si las dimensiones son compatibles para la multiplicación

if cols_A != rows_B:

raise ValueError("Las dimensiones de las matrices no son compatibles para la multiplicación.")

# Inicializar la matriz resultante

result = np.zeros((rows_A, cols_B))

# Algoritmo de multiplicación de matrices

for i in range(rows_A):

for j in range(cols_B):

for k in range(cols_A):

result[i][j] += A[i][k] * B[k][j]

return result

# Ejemplo de dos matrices para multiplicar

matrix_A = np.array([[1, 2],

[3, 4]])

matrix_B = np.array([[5, 6],

[7, 8]])

# Multiplicación de matrices
result_matrix = matrix_multiplication(matrix_A, matrix_B)

print("El resultado de la multiplicación es:")

print(result_matrix)

También podría gustarte