DEV Community

Cover image for La ciencia de los algoritmos
Dennys José Márquez Reyes
Dennys José Márquez Reyes

Posted on • Edited on

La ciencia de los algoritmos

Una guía completa para entender y analizar los algoritmos

Tabla de contenido

1) Introducción

2) Clasificación de los algoritmos

3) Algoritmos condicionales

4) Algoritmos iterativos

5) Algoritmos recursivos

6) Algoritmos de grafos

7) Grafos ponderados y no ponderados

8) Algoritmo de búsqueda en amplitud (BFS)

9) Algoritmo de Dijkstra

10) Algoritmo de búsqueda en profundidad (DFS)

 

Nota:
 
👇 Módulos Aun en desarrollo 👇

 

11) Algoritmos de optimización

12) Algoritmo de optimización por enjambre de partículas (PSO)

13) Algoritmo del recocido simulado (SA)

14) Algoritmo de colonia de hormigas (ACO)

15) Algoritmo Voraz o Greedy

16) Algoritmos de ordenamiento y búsqueda

17) Algoritmos de reducción dimensional

18) Algoritmos en la inteligencia artificial y el aprendizaje automático

19) Diseño y análisis de algoritmos

20) Lenguajes de programación y algoritmos

21) Aplicaciones de los algoritmos

22) Comparación entre los tipos de algoritmos

23) Futuro de los algoritmos

24) - FIN -


Introducción

 

Definición de algoritmos

Un algoritmo es un conjunto de pasos ordenados y precisos que se utilizan para resolver un problema o llevar a cabo una tarea de manera sistemática y eficiente.

Los algoritmos se utilizan en muchos campos, como la informática, la matemática, la física, la ingeniería, la biología y la economía, entre otros, para resolver problemas y automatizar tareas.

Los algoritmos pueden ser tan simples como una receta de cocina o tan complejos como un programa de inteligencia artificial.

Un buen algoritmo debe tener ciertas características para ser considerado eficiente y efectivo.

Ciertas características son fundamentales para que un algoritmo sea efectivo. Algunas de estas características son:

  1. Todos los algoritmos deben de tener un inicio, un proceso y un fin:
  • Inicio: es el punto de partida de un algoritmo, donde se inicia el proceso de resolver un problema o realizar una tarea. Aquí es donde el algoritmo inicializa sus variables y establece los datos de entrada y se prepara el algoritmo para comenzar la ejecución de las instrucciones.  
  • Proceso: son los pasos que sigue el algoritmo para resolver el problema. Aquí es donde el algoritmo hace el trabajo.  
  • Fin: es donde el algoritmo termina. Aquí es donde el algoritmo devuelve la salida o termina, un algoritmo culmina con una solución o salida que se obtiene después de seguir los pasos sucesivos del proceso.

El principio, el proceso y el final de un algoritmo son partes importantes del algoritmo y no deben ser alterados, ya que esto afectaría su capacidad para resolver problemas de manera eficiente y estructurada.

Un ejemplo de algoritmo en pseudocódigo:

  • Pedir al usuario un número entero
  • Si el número es par, dividirlo por 2
  • Si el número es impar, multiplicarlo por 3 y sumarle 1
  • Repetir los pasos 2 y 3 hasta que el resultado sea 1
  • Mostrar todos los números generados en el proceso, incluyendo el número inicial y el 1 final

Este algoritmo se conoce como la Conjetura de Collatz y es un problema matemático que consiste en tomar cualquier número entero positivo y aplicarle una serie de operaciones para llegar eventualmente al número 1.

El algoritmo tiene un principio, un proceso y un final.

El principio del algoritmo pide al usuario un número entero.

El proceso del algoritmo verifica si el número es par o impar y realiza una operación específica en consecuencia.

El proceso se repite hasta que el resultado es 1.

El final del algoritmo muestra todos los números generados en el proceso, incluyendo el número inicial y el 1 final.

  1. Definido: un algoritmo debe estar bien definido, lo que significa que cada paso debe ser claramente definido y entendido. Además, debe ser claro, qué datos de entrada debe tomar y qué resultados debe producir.  
  2. Precisión: un algoritmo debe ser preciso y detallado, es decir, debe especificar claramente cada paso que se debe seguir para resolver el problema o llevar a cabo la tarea.

  3. Finitud: un algoritmo debe ser finito, es decir, debe terminar en un número finito de pasos. Esto significa que no puede ser infinito ni puede repetirse indefinidamente, los algoritmos tienen un número determinado de pasos que deben ser seguidos para llegar a la solución.

  4. Efectividad: un algoritmo debe ser efectivo, lo que significa que debe ser capaz de resolver el problema o llevar a cabo la tarea para la cual fue diseñado de manera eficiente.

  5. Generalidad: un algoritmo debe ser lo suficientemente general para ser aplicado a diferentes situaciones y problemas similares.

  6. Abstractos: los algoritmos son modelos o guías para ordenar procesos y no están limitados a un lenguaje de programación específico.

Importancia de los algoritmos en la informática y otras áreas

Los algoritmos son importantes en la informática y otras áreas por varias razones.

En primer lugar, permiten resolver problemas de manera eficiente. Al diseñar algoritmos óptimos, podemos reducir la complejidad de un problema y encontrar soluciones más rápidas y efectivas.

Los algoritmos son esenciales en la optimización de procesos. En campos como la logística, las finanzas y la producción, los algoritmos ayudan a mejorar la eficiencia y reducir costos.

Por ejemplo, los algoritmos de enrutamiento en la logística optimizan la distribución de productos, minimizando la distancia recorrida y ahorrando tiempo y recursos.

Otra área en la que los algoritmos desempeñan un papel crucial es la inteligencia artificial.

Los algoritmos de aprendizaje automático (machine learning) y las redes neuronales son fundamentales para el desarrollo de sistemas inteligentes que pueden aprender de datos y tomar decisiones autónomamente.

Estos algoritmos permiten reconocer patrones, clasificar información y desarrollar modelos predictivos.

La importancia de los algoritmos radica en su capacidad para permitir la resolución de problemas de manera rápida, sistemática y eficiente, lo que puede ser especialmente importante cuando se trata de grandes conjuntos de datos o problemas complejos.

Aprender los algoritmos nos brinda una base sólida para comprender cómo funcionan los programas de computadora y otros sistemas computacionales.

Comprender los algoritmos nos permite analizar, diseñar y optimizar programas de computadora y sistemas de manera más efectiva, lo que puede mejorar su rendimiento y eficiencia.

Aprender los algoritmos también es útil para el desarrollo de habilidades de pensamiento crítico y resolución de problemas. Los algoritmos nos brindan un enfoque sistemático para resolver problemas, lo que puede ser aplicado en una amplia variedad de situaciones en la vida diaria.

Clasificación de los algoritmos

 

Algoritmos secuenciales

 
Los algoritmos secuenciales son aquellos que se ejecutan de forma lineal, siguiendo un orden concreto y sin saltos. Esto significa que cada paso del algoritmo debe ejecutarse antes de poder ejecutar el siguiente. Los algoritmos secuenciales suelen denominarse algoritmos en serie.

Los algoritmos secuenciales son el tipo de algoritmo más sencillo. Son fáciles de entender e implementar, y suelen ser eficientes para problemas pequeños.

Ejemplos de algoritmos secuenciales

 
Existen varios tipos de algoritmos secuenciales. Algunos tipos comunes de algoritmos secuenciales son:

  • Algoritmos de ordenación: Los algoritmos de ordenación se utilizan para ordenar una lista de elementos en un orden específico. Algunos algoritmos de ordenación comunes son la ordenación burbuja, la ordenación por inserción y Quicksort.

  • Algoritmos de búsqueda: Los algoritmos de búsqueda se utilizan para encontrar un elemento específico en una lista. Algunos algoritmos de búsqueda habituales son la búsqueda lineal, la búsqueda binaria y la búsqueda en tablas hash o mapas hash.

Algunos detalles adicionales sobre los algoritmos secuenciales:

Los algoritmos secuenciales no permiten el paralelismo. Esto significa que el algoritmo no puede ejecutarse en varios procesadores al mismo tiempo.

Los algoritmos secuenciales suelen ser más fáciles de entender e implementar que otros tipos de algoritmos. Esto se debe a que son más simples y no requieren tanta coordinación entre las diferentes partes del algoritmo.

Los algoritmos secuenciales pueden ser ineficientes para problemas grandes.

Esto se debe a que sólo pueden utilizar un procesador a la vez, y a medida que aumenta el tamaño del problema, el tiempo que se tarda en ejecutar el algoritmo puede aumentar exponencialmente.

Ventajas y desventajas de los algoritmos secuenciales

 
Ventajas:

Sencillos de entender e implementar, los algoritmos secuenciales suelen ser más sencillos de entender e implementar que otros tipos de algoritmos, como los algoritmos paralelos. Esto se debe a que no tienen que preocuparse por la concurrencia o el paralelismo.

Eficientes para problemas pequeños, los algoritmos secuenciales pueden ser eficientes para problemas pequeños, especialmente si están bien diseñados.

Fáciles de depurar. Los algoritmos secuenciales suelen ser más fáciles de depurar que otros tipos de algoritmos. Esto se debe a que son más simples y hay menos cosas que pueden ir mal.

Desventajas:

Ineficientes para problemas grandes, los algoritmos secuenciales pueden ser ineficientes para problemas grandes, especialmente si no están bien diseñados.

Esto se debe a que sólo pueden utilizar un procesador a la vez.

No pueden aprovechar el procesamiento paralelo, los algoritmos secuenciales no pueden aprovechar el procesamiento paralelo para acelerar su ejecución.

Esto puede ser una gran desventaja para problemas grandes.

No son tan flexibles como otros tipos de algoritmos, los algoritmos secuenciales no son tan flexibles como otros tipos de algoritmos, como los algoritmos paralelos.

Esto se debe a que están limitados a ejecutarse de forma lineal.

Los algoritmos secuenciales son una buena opción para problemas pequeños o que no requieren mucha capacidad de procesamiento.

Para problemas más grandes o que requieran mucha capacidad de procesamiento, los algoritmos paralelos pueden ser una mejor opción.
 

Algoritmos condicionales

 
Los algoritmos condicionales son la clave para tomar decisiones en función de una o varias condiciones específicas. Su funcionamiento se basa en estructuras de control como "if-else", "switch-case", "Operador ternario", que permiten a los programadores controlar el flujo de ejecución de un programa y tomar decisiones en consecuencia.

En el mundo de la programación, los algoritmos condicionales son herramientas imprescindibles para garantizar que los programas funcionen correctamente y cumplan con sus objetivos. Su capacidad para evaluar condiciones y tomar decisiones en consecuencia los diferencia de otros tipos de algoritmos y los hace fundamentales para controlar el flujo de ejecución de un programa de manera eficiente y efectiva.

  • Puede utilizar una sentencia if-else para comprobar si un número es par o impar.
number = 10

if (number % 2 == 0):
  print ("El número es par")
else:
  print ("El número es impar")
Enter fullscreen mode Exit fullscreen mode
  • Puede utilizar una sentencia switch case para imprimir los días de la semana.
day = "Viernes"

switch (day):
  case "Lunes":
    print ("El día es lunes")
    break
  case "Martes":
    print ("El día es Martes")
    break
  case "Miércoles":
    print ("El día es Miércoles")
    break
  case "Jueves":
    print ("El día es Jueves")
    break
  case "Viernes":
    print ("El día es Viernes")
    break
  case "Sábado":
    print ("El día es Sábado")
    break
  case "Domingo":
    print ("El día es Domingo")
    break
  default:
    print ("El día no es válido")
Enter fullscreen mode Exit fullscreen mode
  • Puedes utilizar un operador ternario para asignar un valor a una variable en función del valor de otra variable.
number = 10

    even_or_odd = number % 2 == 0 ? "par" : "impar"

print (even_or_odd)

Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos condicionales

 
Ventajas:

Permite a los programadores controlar el flujo de ejecución de un programa en función de ciertas condiciones.

Ayuda a optimizar el rendimiento del programa al evitar la ejecución de instrucciones innecesarias.

Permite la toma de decisiones en tiempo real y la adaptación del programa a diferentes situaciones.

Facilita la legibilidad y la comprensión del código, ya que permite una estructuración lógica y clara.

Desventajas:

Pueden resultar confusos y complejos si se anidan demasiado, lo que puede dificultar la lectura y el mantenimiento del código.

Si se utilizan de manera incorrecta o se anidan demasiado, pueden provocar errores en el programa.

Pueden afectar al rendimiento del programa si se utilizan de manera ineficiente o si se realizan muchas comprobaciones de condición.

La complejidad del programa puede aumentar si se utilizan muchas estructuras de control condicionales.
 

Algoritmos iterativos

 
Los algoritmos iterativos son un tipo de algoritmo que repite un conjunto de instrucciones hasta que se cumple una determinada condición. Esto contrasta con los algoritmos recursivos, que se llaman a sí mismos para resolver un problema.

Los algoritmos iterativos se utilizan a menudo para resolver problemas que implican bucles, como encontrar el factorial de un número o imprimir la secuencia de Fibonacci. También se utilizan para resolver problemas de búsqueda, como encontrar el valor mínimo o máximo de una matriz.

Las estructuras de repetición o bucles más comunes son Do While, While y For.

Ejemplos de algoritmos iterativos

  • Do While, la estructura de repetición Do While ejecuta un bloque de código al menos una vez y luego repite la ejecución del bloque mientras se cumpla una condición. La condición se evalúa después de la primera ejecución del bloque de código.

Imprime los números del 1 al 10.

i = 1
repeat
    print(i)
    i = i + 1
until i > 10
Enter fullscreen mode Exit fullscreen mode
  • While, la estructura de repetición While ejecuta un bloque de código mientras se cumpla una condición. La condición se evalúa al principio del ciclo.

Este algoritmo buscar un número en una lista. El ciclo se ejecuta mientras el valor de i es menor que el tamaño de la lista y el número no ha sido encontrado.

Si el elemento actual de la lista es igual al número buscado, se establece la variable encontrado en true y el ciclo se detiene. De lo contrario, se incrementa el valor de i para pasar al siguiente elemento de la lista.

Cálculo del factorial de un número.

lista = [2, 4, 6, 8, 10]
n = length(lista)
num_buscado = 8
i = 0
encontrado = false
while i < n and not encontrado do
    if lista[i] == num_buscado then
        encontrado = true
    else
        i = i + 1
    end if
end while
Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos iterativos

 
Ventajas:

Suelen ser más fáciles de entender y escribir que los algoritmos recursivos.

Pueden ser más eficientes que los algoritmos recursivos en algunos casos.

Pueden utilizarse para resolver problemas que los algoritmos recursivos no pueden resolver.

Desventajas:

Pueden ocupar más espacio que los algoritmos recursivos.

Pueden ser más difíciles de depurar que los algoritmos recursivos.

En algunos casos pueden ser menos eficientes que los algoritmos recursivos.

Algoritmos recursivos

 
Los algoritmos recursivos son aquellos que se invocan a sí mismos de manera directa o indirecta. Cabe destacar que la recursión es una técnica de programación que se utiliza en la implementación de estos algoritmos.

La llamada recursiva es el proceso de llamar a la misma función desde dentro de la función.

Esto significa que el algoritmo resuelve un problema dividiéndolo en problemas más pequeños y llamándose a sí mismo para resolverlos.

Cada vez que se realiza una llamada recursiva, se apila en memoria para que la función pueda retomar la ejecución de la llamada anterior una vez que se ha resuelto el subproblema actual.

La estructura de datos de la pila es una estructura de datos LIFO (último en entrar, primero en salir). Esto significa que el último elemento que se introdujo en la pila es el primer elemento que se extraerá de la pila.

El proceso de llamadas recursivas y apilamiento en memoria continúa hasta que se llega a un punto en el que no se necesita hacer más llamadas recursivas para resolver el problema.

Cuando la función recursiva termina, sale de la pila. Esto permite al algoritmo volver al estado en el que se encontraba antes de realizar la llamada recursiva.

La estructura de datos de la pila es una poderosa herramienta para los algoritmos recursivos. Permite a los algoritmos recursivos resolver problemas que serían difíciles o imposibles de resolver sin ella.

Los algoritmos recursivos se utilizan a menudo para implementar estructuras de datos recursivas, como árboles y grafos.

Estas estructuras de datos pueden ser difíciles o imposibles de implementar con algoritmos iterativos.

Ejemplos de algoritmos recursivos

  • Búsqueda lineal recursiva: Este algoritmo busca un elemento específico en un array.
def busqueda_lineal(array, item):
  if len(array) == 0:
    return -1
  elif array[-1] == item:
    return len(array) - 1
  else:
    return busqueda_lineal(array[:-1], item)
Enter fullscreen mode Exit fullscreen mode
  • Suma recursiva de elementos de una matriz: Este algoritmo calcula la suma de todos los elementos de un array.
def suma_de_arreglo(array):
  if len(array) == 0:
    return 0
  else:
    return array[0] + suma_de_arreglo(array[1:])
Enter fullscreen mode Exit fullscreen mode
  • Menú anidado recursivo: Este algoritmo imprime recursivamente el contenido de un menú anidado.
def print_menu(menus, level=0):
  for item in menus:
    print("|", "-" * level * 2, item["title"])
    # Imprime recursivamente los children
    if item.get("children") and len(item.get("children")) > 0:
      print_menu(item.get("children"), level + 1)


print_menu([
  {
    "title": "Item 1",
    "children": [
      {
        "title": "Item 1.1",
        "children": [
          {
            "title": "Item 1.1.1"
          }
        ]
      },
      {
        "title": "Item 1.2"
      }
    ]
  },
  {
    "title": "Item 2",
    "children": [
      {
        "title": "Item 2.1"
      }
    ]
  }
])

Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos recursivos

 
Ventajas:

  • Simplicidad: Los algoritmos recursivos pueden ser más fáciles de entender y escribir que los algoritmos iterativos.

Esto se debe a que los algoritmos recursivos descomponen un problema en subproblemas cada vez más pequeños hasta llegar a un caso base.

  • Expresividad: Los algoritmos recursivos pueden utilizarse para expresar problemas que son difíciles o imposibles de expresar con algoritmos iterativos.

Por ejemplo, los algoritmos recursivos pueden utilizarse para resolver problemas que impliquen recorrer un árbol y retroceder.

  • Eficacia: Los algoritmos recursivos pueden ser eficientes para ciertos problemas.

Por ejemplo, los algoritmos recursivos se utilizan a menudo para implementar algoritmos de tipo divide y vencerás, Ej. los algoritmos "Merge Sort", "Quick Sort" , "Búsqueda Binaria", entre otros.

Los algoritmos recursivos que pueden ser muy eficientes para problemas de ordenación y búsqueda.

  • Recursividad de cola: La recursividad de cola es un tipo especial de recursividad en el que la llamada recursiva es lo último que hace la función.

El compilador puede optimizar la recursividad de cola, lo que puede mejorar significativamente el rendimiento.

Desventajas:

  • Espacio en la pila: Los algoritmos recursivos pueden utilizar mucho espacio de pila, especialmente para problemas con recursión profunda. Esto se debe a que cada llamada recursiva crea un nuevo marco de pila, y estos marcos de pila pueden sumarse rápidamente.

  • Complejidad de la implementación: La recursión puede ser más difícil de implementar correctamente que los algoritmos iterativos, especialmente para programadores principiantes.

  • Recursión infinita: La recursión infinita ocurre cuando una función recursiva se llama a sí misma sin forma de terminar.

La recursividad infinita es un error común en los algoritmos recursivos, y puede ser difícil de depurar y corregir.

  • Propensos a errores: Los algoritmos recursivos pueden ser más propensos a errores que los algoritmos iterativos. Esto se debe a que es fácil cometer errores cuando se rastrea el estado de la recursión.

Los algoritmos recursivos pueden ser más difíciles de depurar que los algoritmos iterativos. Si la recursión es demasiado profunda, puede ser difícil rastrear el origen de un error.

Ten en cuenta que los algoritmos recursivos también pueden tener un mayor costo de tiempo de ejecución (mayor cantidad de llamadas recursivas y apilamiento en memoria pueden ser más costosos que un algoritmo iterativo equivalente), y en algunos casos la recursión puede no ser la mejor opción para resolver un problema específico.

Es importante evaluar cuidadosamente si la recursión es la mejor opción para resolver un problema determinado, y si es así, diseñar el algoritmo de manera que minimice la cantidad de llamadas recursivas y la profundidad de la recursión.

También es importante considerar la posibilidad de convertir un algoritmo recursivo en un algoritmo iterativo si se encuentra que la recursión es demasiado costosa en términos de memoria o tiempo de ejecución.

Algoritmos de grafos

 

Definición de grafos

Un Grafo como estructura de datos es una representación gráfica de un conjunto de elementos o entidades y sus relaciones entre si.

Un grafo es una estructura matemática formada por un conjunto de nodos (también llamados vértices) y un conjunto de aristas (también llamadas enlaces o líneas) que conectan pares de nodos. Los nodos pueden representar cualquier cosa, como personas, ciudades u ordenadores.

Las aristas representan las conexiones o relaciones entre los nodos, como enlaces de comunicación o carreteras.

Los grafos se utilizan a menudo para representar redes sociales, redes de transporte y redes informáticas.

Definición de algoritmos grafos

 
Un algoritmo de grafos es un conjunto de instrucciones o procedimientos que se utilizan para resolver problemas relacionados con los grafos.

Estos algoritmos pueden ser diseñados para realizar diferentes tareas, como encontrar la ruta más corta entre dos nodos, determinar si un grafo es conexo o no, entre otros.

Los algoritmos de grafos se utilizan para resolver una gran variedad de problemas:

  • Encontrar el camino más corto entre dos nodos: Se trata de un problema habitual en los algoritmos de enrutamiento.

Por ejemplo, un algoritmo de grafos puede utilizarse para encontrar el camino más corto entre dos ciudades en una red de carreteras.

  • Encontrar los componentes conectados de un grafo: Es un problema del análisis de redes sociales.

Por ejemplo, se puede utilizar un algoritmo de grafos para encontrar los distintos grupos de personas conectados entre sí en una red social.

  • Encontrar el árbol mínimo de un grafo: Se trata de un problema de visión por ordenador.

Por ejemplo, un algoritmo de grafos podría utilizarse para hallar el árbol mínimo de una red de carreteras, que a su vez podría utilizarse para hallar el camino más corto entre dos nodos de la red.

La operación básica de un algoritmo de grafos es recorrer el grafo y visitar cada nodo.

El algoritmo también puede llevar un registro de las aristas visitadas, la distancia a cada nodo u otra información.

El funcionamiento específico de un algoritmo depende del problema que intente resolver.

Componentes de un grafo: nodos y aristas

 
Nodos: Los nodos de una red representan los objetos o entidades que se conectan. En una red social, los nodos representan personas. En una red de transporte, los nodos representan ciudades o intersecciones.

Aristas: Las aristas de una red representan las conexiones entre los nodos. En una red social, las aristas representan amistades o relaciones. En una red de transporte, las aristas representan carreteras o vías férreas.

El peso que puede tener una de una arista representar varias cosas, como la distancia entre dos nodos, el coste de la comunicación entre dos ordenadores o la fuerza de una relación entre dos personas.

Grafos dirigidos y no dirigidos

 

  • Grafo dirigido: es un grafo en el que cada arista tiene asociada una dirección, esto significa que la arista sólo puede recorrerse en una dirección.

Por ejemplo, en una red social, una arista dirigida de A a B significa que A es amigo de B, pero B no es amigo de A.

  • Grafo no dirigido: es un grafo en el que las aristas no tienen una dirección asociada, esto significa que la arista puede recorrerse en cualquier dirección.

Por ejemplo, en una red de transporte, una arista no dirigida entre dos ciudades significa que hay una carretera o ferrocarril que conecta las dos ciudades en ambas direcciones.

La decisión de utilizar un grafo dirigido o no dirigido depende de la aplicación concreta.

Por ejemplo, un grafo dirigido sería la mejor opción para modelar una red social porque la dirección de las aristas es importante (por ejemplo, A es amigo de B, pero B no es amigo de A).

Un grafo no dirigido sería la mejor opción para modelar una red de transporte porque la dirección de las aristas no es importante (por ejemplo, hay una carretera que conecta la ciudad A con la ciudad B, y también hay una carretera que conecta la ciudad B con la ciudad A).

Grafos bipartitos

 

Definición de grafos bipartitos

Un grafo bipartito es un grafo cuyos vértices pueden dividirse en dos conjuntos disjuntos, de forma que no hay dos vértices adyacentes dentro del mismo conjunto. Esto significa que cada arista del grafo conecta un vértice de un conjunto con un vértice del otro conjunto.

Propiedades y características importantes de los grafos bipartitos

 
Los grafos bipartitos tienen varias propiedades interesantes y útiles.

  • Un grafo es bipartito si no tiene ciclos impares, es decir cada ciclo de un grafo bipartito debe tener longitud par.

Es una propiedad fundamental de los grafos bipartitos y se puede usar para determinar si un grafo es bipartito o no. El teorema establece que un grafo es bipartito si y solo si todos sus ciclos son de longitud par.

  • Los grafos bipartitos tienen una estructura simple que permite la resolución eficiente de muchos problemas de optimización, como el problema de emparejamiento máximo.

  • Un grafo bipartito es bicolorable, es decir, se puede colorear con dos colores de forma que no haya dos vértices adyacentes del mismo color.

La máxima coincidencia en un grafo bipartito es igual a la mínima cobertura de vértices en el grafo.

Algoritmos para determinar si un grafo es bipartito (por ejemplo, el algoritmo de coloración de grafos)

 
Existen varios algoritmos para determinar si un grafo es bipartito. Uno de los más comunes es el algoritmo de coloreado de grafos. Este algoritmo asigna colores a los vértices del grafo de forma que no haya dos vértices adyacentes del mismo color. Si el grafo se puede colorear con dos colores, entonces es bipartito.

Otro algoritmo que se puede utilizar para determinar si un grafo es bipartito es el algoritmo de correspondencia bipartita.

Este algoritmo funciona encontrando una máxima coincidencia en el grafo. Si el máximo emparejamiento en el grafo tiene el mismo número de aristas que el número de vértices en uno de los dos conjuntos, entonces el grafo es bipartito.

Algoritmos para encontrar un emparejamiento máximo en un grafo bipartito (por ejemplo, el algoritmo de emparejamiento máximo de Hopcroft-Karp)

 
Existen varios algoritmos para encontrar el máximo emparejamiento en un grafo bipartito.

Un algoritmo común es el algoritmo de emparejamiento máximo de Hopcroft-Karp.

Este algoritmo funciona encontrando iterativamente caminos de aumento en el grafo.

Un camino de aumento es un camino en el grafo tal que los puntos finales del camino no están emparejados, y todos los demás vértices del camino están emparejados.

El algoritmo de Hopcroft-Karp puede encontrar la máxima coincidencia en el grafo mediante la búsqueda iterativa de caminos de aumento.

Ejemplos de aplicaciones prácticas de los grafos bipartitos

 
Los grafos bipartitos tienen numerosas aplicaciones prácticas. Algunos ejemplos de aplicaciones prácticas de los grafos bipartitos son:

  • Problemas de emparejamiento: Los grafos bipartitos se pueden utilizar para modelar problemas de emparejamiento, como el problema de encontrar el máximo número de personas que se pueden emparejar con puestos de trabajo.

  • Problemas de programación: Los grafos bipartitos pueden utilizarse para modelar problemas de programación, como el problema de encontrar el número máximo de tareas que pueden programarse en un conjunto de máquinas.

  • Algoritmos genéticos: Los grafos bipartitos pueden utilizarse para modelar algoritmos genéticos, que son un tipo de algoritmo de búsqueda que puede utilizarse para encontrar soluciones a problemas de optimización.

  • Redes de interacción proteína-proteína: Los grafos bipartitos pueden utilizarse para modelar redes de interacción proteína-proteína, en las que un conjunto de vértices representa proteínas y el otro conjunto de vértices representa interacciones entre proteínas.

  • Sistemas de recomendación de películas: Los grafos bipartitos pueden utilizarse para modelar sistemas de recomendación de películas, en los que un conjunto de vértices representa a las películas y el otro conjunto de vértices representa a los usuarios.

Grafos ponderados y no ponderados

 

  • Grafo ponderado: es un grafo en el que cada arista tiene un peso o valor asociado que indica la distancia o el costo de la conexión entre los nodos que conecta. En un grafo ponderado, los pesos de las aristas pueden ser números reales o enteros, dependiendo del problema que se esté resolviendo.

Los grafos ponderados se utilizan comúnmente en problemas de optimización, como encontrar la ruta más corta entre dos nodos, encontrar el árbol generador mínimo, encontrar el emparejamiento máximo, entre otros.

  • Grafo no ponderado: es un grafo en el que todas las aristas tienen el mismo peso, es decir, no hay pesos asociados a las aristas. En un grafo no ponderado, todas las aristas se consideran igualmente importantes y no hay una medida de distancia o costo asociada a las conexiones entre los nodos.

Los grafos no ponderados se utilizan comúnmente en problemas de conectividad, como encontrar los componentes conectados de un grafo, determinar si un grafo es bipartito, encontrar ciclos en un grafo, entre otros.

Algoritmo de búsqueda en amplitud (BFS)

 
El algoritmo de búsqueda en amplitud (BFS, por sus siglas en inglés) es un algoritmo de búsqueda que se utiliza para recorrer todos los nodos de un Grafo en orden de distancia desde un nodo inicial.

Encontrar el camino más corto entre dos nodos: BFS puede utilizarse para encontrar el camino más corto entre dos nodos de un grafo. Esto se debe a que BFS explora el grafo capa por capa, lo que garantiza que el camino más corto se encuentre siempre en primer lugar.

El algoritmo comienza en el nodo inicial y explora todos los nodos vecinos en la profundidad actual antes de pasar a los nodos en el siguiente nivel de profundidad.

Esto se repite hasta que se visitan todos los nodos del Grafo.

Cómo funciona BFS:

BFS funciona manteniendo una cola de nodos que aún no han sido visitados.

El algoritmo comienza visitando el nodo raíz (o nodo inicial) y lo agrega a la cola de nodos por visitar. Luego, el algoritmo saca el primer nodo de la cola y visita todos sus nodos adyacentes (o vecinos), agregándolos a la cola si aún no han sido visitados.

El proceso se repite hasta que la cola está vacía, lo que significa que todos los nodos del Grafo han sido visitados. El algoritmo BFS garantiza que los nodos se visiten en orden de distancia desde el nodo raíz, es decir, primero se visitan los nodos a una distancia de uno, luego los nodos a una distancia de dos, y así sucesivamente.

El algoritmo BFS puede ser modificado para encontrar el camino más corto entre dos nodos en un Grafo no ponderado. En este caso, el algoritmo sigue el mismo proceso mencionado anteriormente, pero también mantiene un registro de la distancia de cada nodo desde el nodo raíz. Una vez que se encuentra el nodo objetivo, se puede reconstruir el camino más corto siguiendo las distancias registradas hacia atrás desde el nodo objetivo hasta el nodo raíz.

Ejemplos de algoritmos de búsqueda en amplitud (BFS)

 

  • Para encontrar el camino más corto entre dos nodos de un Grafo, se recorrer el Grafo capa por capa, manteniendo un registro de los nodos visitados y el camino actual desde el nodo de inicio hasta el nodo actual.

Una vez que se encuentra el nodo de destino, la función devuelve el camino más corto desde el nodo de inicio hasta el nodo de destino.

En este caso, el camino más corto entre el nodo 'A' y el nodo 'X' es ['A', 'C', 'F', 'X'].

from collections import deque

# Definir la función BFS para encontrar el camino más corto entre dos nodos
def bfs_shortest_path(graph, start, end):
    # Crear una cola para mantener los nodos por visitar
    queue = deque([(start, [start])])
    # Crear una lista para mantener un registro de los nodos visitados
    visited = set()

    # Mientras la cola no esté vacía
    while queue:
        # Sacar el primer nodo de la cola
        node, path = queue.popleft()
        # Si el nodo no ha sido visitado
        if node not in visited:
            # Marcar el nodo como visitado
            visited.add(node)
            # Si el nodo es el nodo objetivo, devolver el camino
            if node == end:
                return path
            # Añadir los vecinos del nodo a la cola
            for neighbor in graph[node]:
                queue.append((neighbor, path + [neighbor]))
    # Si no se encontró el camino, devolvera None
    return None

# Ejemplo de uso
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'X']
}
start = 'A'
end = 'X'
shortest_path = bfs_shortest_path(graph, start, end)
print(f"El camino más corto entre {start} y {end} es: {shortest_path}")
Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos de búsqueda en amplitud (BFS)

 
Ventajas:

  • Fácil de implementar: BFS es un algoritmo relativamente sencillo de implementar. Esto lo convierte en una buena opción para problemas en los que la velocidad no es crítica.

  • Eficaz para encontrar componentes conectados: BFS es muy eficiente para encontrar componentes conectados en un grafo. Esto se debe a que BFS explora el grafo capa por capa, lo que garantiza que se encuentren todos los componentes conectados.

  • Eficacia para recorrer árboles: BFS es muy eficiente para recorrer árboles. Esto se debe a que los árboles son un tipo especial de grafo que se puede recorrer muy rápidamente utilizando BFS.

  • Complejidad temporal: La complejidad temporal de BFS es O(V+E), donde V es el número de nodos en el grafo y E es el número de aristas. Esto lo convierte en un algoritmo eficiente en términos de tiempo para grafos pequeños y medianos.

Desventajas:

  • No es tan eficiente para encontrar los caminos más cortos: BFS no es tan eficiente como otros algoritmos, como el algoritmo de Dijkstra, para encontrar los caminos más cortos en un grafo. Esto se debe a que el BFS explora el grafo capa por capa, lo que puede llevar a encontrar caminos más largos.

  • Complejidad espacial: La complejidad espacial de BFS es O(V), donde V es el número de nodos en el grafo. Esto puede ser un problema en grafos grandes con muchos nodos, ya que puede requerir una gran cantidad de memoria.

  • No es tan eficaz en grafos grandes: BFS puede ser ineficaz para grafos grandes. Esto se debe a que BFS necesita explorar todo el grafo, lo que puede llevar mucho tiempo en grafos grandes.

  • No funciona para grafos con pesos negativos: BFS no funciona para grafos con pesos negativos, ya que no garantiza encontrar el camino más corto en este tipo de grafos.

Algoritmo de Dijkstra

 
El algoritmo de Dijkstra es un algoritmo de búsqueda de camino más corto en grafos dirigidos o no dirigidos con pesos no negativos en las aristas. Fue desarrollado por el científico holandés Edsger W. Dijkstra en 1956. El algoritmo de Dijkstra encuentra el camino más corto desde un nodo de origen a todos los demás nodos en un grafo.

Cómo funciona Dijkstra:

El algoritmo funciona mediante la asignación de una distancia a cada nodo en el grafo. Inicialmente, la distancia del nodo de origen se establece en cero y la distancia de todos los demás nodos se establece en infinito. Luego, el algoritmo visita cada nodo en el grafo en orden de distancia, actualizando las distancias de los nodos adyacentes si se encuentra un camino más corto. El proceso continúa hasta que se han visitado todos los nodos.

El algoritmo de Dijkstra avanzar hacia el verdadero camino más corto. Si todas las aristas del grafo tuvieran peso negativo, el algoritmo nunca podría encontrar el verdadero camino más corto, ya que siempre se quedaría atascado en un ciclo negativo.

Las aristas específicas que tienen peso no negativo dependen del grafo específico que se esté utilizando y para ese tipo de grafo existe el algoritmo de Bellman-Ford.

El algoritmo de Bellman-Ford también funciona en grafos con pesos no negativos, pero en este caso, el algoritmo de Dijkstra es más eficiente que el algoritmo de Bellman-Ford.

Ejemplos de algoritmos de Dijkstra

 
Para aclarar, el algoritmo de Dijkstra calcula la distancia más corta desde un nodo de origen a todos los demás nodos en el grafo. En este ejemplo, el nodo de origen es A, y la distancia más corta desde A a sí mismo es 0 (A = 0). La distancia más corta desde A hasta el nodo B es 2 (A -> B = 2), la distancia más corta desde A hasta el nodo C es 6 (A -> B -> E -> F -> C = 6), y así sucesivamente.

La distancia más corta desde A hasta D es 1 (A -> D = 1), y la distancia más corta desde A hasta E es 5 (A -> B -> E = 5). La distancia más corta desde A hasta F es 8 (A -> B -> E -> F = 8).

{'A': 0, 'B': 2, 'C': 6, 'D': 1, 'E': 5, 'F': 8}

import heapq

def dijkstra(graph, start):
    # Inicializa un diccionario para mantener un seguimiento de las distancias desde el nodo de inicio a cada nodo en el grafo
    distances = {node: float("inf") for node in graph}
    # Establece la distancia del nodo de inicio a sí mismo como 0
    distances[start] = 0
    # Inicializa una cola de prioridad (montón binario) y agrega el nodo de inicio con una distancia de 0
    queue = []
    heapq.heappush(queue, (0, start))

    while queue:
        # Extrae el nodo con la distancia más corta de la cola de prioridad
        (cost, node) = heapq.heappop(queue)

        # Si la distancia registrada desde el nodo de inicio al nodo actual es menor que la distancia actual, continua con el siguiente nodo
        if distances[node] < cost:
            continue

        # Recorre cada nodo adyacente al nodo actual
        for neighbor, weight in graph[node].items():
            # Calcula la distancia desde el nodo de inicio al nodo adyacente a través del nodo actual
            new_cost = cost + weight
            # Si la nueva distancia es menor que la distancia registrada actualmente desde el nodo de inicio al nodo adyacente, actualiza la distancia
            # y agrega el nodo adyacente a la cola de prioridad para su posterior exploración
            if new_cost < distances[neighbor]:
                distances[neighbor] = new_cost
                heapq.heappush(queue, (new_cost, neighbor))

    # Devuelve el diccionario de distancias resultante
    return distances

# Ejemplo de uso
graph = {
    'A': {'B': 2, 'D': 1},
    'B': {'A': 2, 'C': 4, 'E': 3},
    'C': {'B': 4, 'F': 2},
    'D': {'A': 1, 'E': 5},
    'E': {'B': 3, 'D': 5, 'F': 6},
    'F': {'C': 2, 'E': 6}
}

# Llama a la función dijkstra con el grafo y el nodo de inicio 'A'
distances = dijkstra(graph, "A")

# Imprime el diccionario de distancias resultante
print(distances)
Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos Dijkstra

 
Ventajas:

  • Es un algoritmo completo, lo que significa que encuentra todas las rutas más cortas desde el nodo de inicio a todos los demás nodos del grafo.

  • Eficacia: El algoritmo de Dijkstra es un algoritmo codicioso, lo que significa que siempre hace la mejor elección posible en cada paso. Esto lo hace muy eficiente para encontrar el camino más corto en un grafo con pesos de arista no negativos.

  • Simplicidad: El algoritmo de Dijkstra es relativamente sencillo de entender e implementar. Esto lo convierte en una buena opción para problemas en los que la simplicidad del algoritmo es importante.

  • Amplia aplicabilidad: El algoritmo de Dijkstra puede utilizarse para encontrar el camino más corto en una gran variedad de grafos, incluyendo grafos dirigidos y no dirigidos, grafos ponderados y no ponderados. Esto lo convierte en un algoritmo versátil que puede aplicarse a una amplia gama de problemas.

Tiene un buen rendimiento para grafos pequeños y medianos.

Desventajas.

  • No funciona bien con grafos con aristas con pesos negativos, ya que puede entrar en un bucle infinito.

  • Requiere una estructura de datos de cola de prioridad para mantener un seguimiento de los nodos más cercanos, lo que puede resultar en un mayor uso de memoria y tiempo de ejecución.

  • Tiene un rendimiento deficiente en grafos grandes o densamente conectados. Esto se debe a que tiene que visitar todos los nodos del grafo para encontrar el camino más corto.

Algoritmo de búsqueda en profundidad (DFS)

 
El algoritmo de búsqueda en profundidad (DFS) es un algoritmo de búsqueda no informada que explora todos los nodos de un grafo o árbol mediante la exploración de un camino completo antes de volver atrás para explorar otro camino. 

La búsqueda no informada se refiere a un tipo de estrategia de búsqueda en la que se evalúa el siguiente nodo sin conocer a priori si este es mejor o peor que el anterior. 

Los algoritmos de búsqueda no informados no tienen información adicional sobre el nodo objetivo que no sea la proporcionada en la definición del problema. 

Los planes para alcanzar el nodo objetivo desde el nodo inicial difieren solo por el orden y la duración de las acciones. 

Cómo funciona DFS:

El algoritmo comienza en el nodo raíz (o cualquier nodo arbitrario) y explora cada uno de sus vecinos en profundidad antes de pasar a los vecinos del siguiente nivel.

Una vez que se alcanza un nodo sin hijos, el algoritmo retrocede al nodo anterior sin explorar y continúa la exploración desde allí.

Ejemplos de algoritmos de búsqueda en profundidad (DFS)

 
Realiza una búsqueda en profundidad (DFS) en un grafo dado, comenzando desde un nodo de inicio dado.

El algoritmo comienza desde el nodo de inicio dado y recorre cada nodo adyacente al nodo actual de manera recursiva hasta que se alcanzan todos los nodos accesibles.

def dfs(graph, start, visited=None):

    # Si visited no se proporciona, inicializa visited como un conjunto vacío
    if visited is None:
        visited = set()

    # Agrega el nodo de inicio a la lista de nodos visitados y lo imprime
    visited.add(start)
    print(start)

    # Recorre cada nodo adyacente al nodo de inicio
    for neighbor in graph[start]:
        # Si el nodo adyacente no ha sido visitado, realiza una búsqueda DFS recursiva desde ese nodo
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

# Ejemplo de uso
graph = {
    "A": ["B", "C"],
    "B": ["D", "E"],
    "C": ["F", "G"],
    "D": [],
    "E": [],
    "F": [],
    "G": [],
}

# Realiza una búsqueda DFS en el grafo con el nodo de inicio "A"
dfs(graph, "A")
Enter fullscreen mode Exit fullscreen mode

Ventajas y desventajas de los algoritmos de búsqueda en profundidad (DFS)

 
Ventajas:

  • El algoritmo DFS es fácil de implementar y entender, ya que utiliza una estrategia simple de exploración recursiva de los nodos de un grafo.

  • DFS es muy útil cuando se busca encontrar un camino que conecte dos nodos en un grafo, ya que recorre un camino completo antes de explorar otros caminos.

  • En algunos casos, DFS puede ser más eficiente que otros algoritmos de búsqueda, ya que utiliza menos memoria que otros algoritmos de búsqueda como el algoritmo de búsqueda en amplitud (BFS).

Desventajas:

  • DFS no garantiza encontrar la solución óptima en grafos con múltiples soluciones.

  • DFS puede quedar atrapado en ciclos infinitos si el grafo tiene ciclos, lo que puede llevar a un tiempo de ejecución infinito.

  • DFS puede explorar nodos innecesarios, lo que puede aumentar el tiempo de ejecución y reducir la eficiencia en grafos grandes o complejos.

  • DFS puede ser menos eficiente que otros algoritmos de búsqueda en grafos con una alta relación entre la profundidad y la amplitud.

---- > Continuará 🤜🤛

Top comments (1)

Collapse
 
dennysjmarquez profile image
Dennys José Márquez Reyes • Edited

¡Hola a todos! Este artículo sobre algoritmos aún está en proceso de ser terminado. Lo estoy escribiendo para refrescar mis conocimientos y ayudar a la comunidad.

Espero que disfruten de la lectura y que sea útil para su aprendizaje 🤜🤛🤓