En esta sección, pondremos en práctica las estrategias de diseño de algoritmos que hemos aprendido en los módulos anteriores. Nos enfocaremos en ejercicios que abarcan las técnicas de "Divide y Vencerás", "Algoritmos Greedy", "Programación Dinámica" y "Backtracking". Cada ejercicio incluirá una descripción del problema, una solución detallada y explicaciones paso a paso.
Ejercicio 1: Divide y Vencerás
Problema: Suma Máxima de Subarreglo
Dado un arreglo de números enteros, encuentra el subarreglo (contiguo) que tiene la suma máxima y devuelve su suma.
Ejemplo:
Entrada: [-2,1,-3,4,-1,2,1,-5,4] Salida: 6 Explicación: El subarreglo [4,-1,2,1] tiene la suma máxima de 6.
Solución:
Usaremos la técnica de "Divide y Vencerás" para resolver este problema.
Paso 1: Dividir
Dividimos el arreglo en dos mitades.
Paso 2: Conquistar
Recursivamente encontramos la suma máxima de subarreglo en las dos mitades.
Paso 3: Combinar
Encontramos la suma máxima de subarreglo que cruza la mitad y combinamos los resultados.
Código:
def max_crossing_sum(arr, left, mid, right): # Incluye elementos en la suma desde el medio hacia la izquierda left_sum = float('-inf') total = 0 for i in range(mid, left-1, -1): total += arr[i] if total > left_sum: left_sum = total # Incluye elementos en la suma desde el medio hacia la derecha right_sum = float('-inf') total = 0 for i in range(mid + 1, right + 1): total += arr[i] if total > right_sum: right_sum = total # Devuelve la suma máxima que cruza el medio return left_sum + right_sum def max_subarray_sum(arr, left, right): # Caso base: solo un elemento if left == right: return arr[left] # Encuentra el punto medio mid = (left + right) // 2 # Devuelve el máximo de las tres posibles sumas return max(max_subarray_sum(arr, left, mid), max_subarray_sum(arr, mid+1, right), max_crossing_sum(arr, left, mid, right)) # Ejemplo de uso arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4] result = max_subarray_sum(arr, 0, len(arr) - 1) print("La suma máxima de subarreglo es:", result)
Explicación:
- max_crossing_sum: Calcula la suma máxima de un subarreglo que cruza el punto medio.
- max_subarray_sum: Utiliza recursión para dividir el arreglo y encontrar la suma máxima en cada mitad y la suma máxima cruzada.
- Caso base: Si el subarreglo tiene un solo elemento, devuelve ese elemento.
- Combinación: Devuelve el máximo de las tres posibles sumas.
Ejercicio 2: Algoritmos Greedy
Problema: Actividades Máximas
Dado un conjunto de actividades con tiempos de inicio y finalización, selecciona el máximo número de actividades que no se solapen.
Ejemplo:
Solución:
Usaremos un enfoque greedy para resolver este problema.
Paso 1: Ordenar
Ordenamos las actividades por su tiempo de finalización.
Paso 2: Seleccionar
Seleccionamos la actividad que termina primero y luego seleccionamos la siguiente actividad que empieza después de que la actividad seleccionada haya terminado.
Código:
def max_activities(activities): # Ordena las actividades por su tiempo de finalización activities.sort(key=lambda x: x[1]) # La primera actividad siempre se selecciona selected_activities = [activities[0]] last_end_time = activities[0][1] # Recorre las actividades restantes for i in range(1, len(activities)): if activities[i][0] >= last_end_time: selected_activities.append(activities[i]) last_end_time = activities[i][1] return selected_activities # Ejemplo de uso activities = [(1, 3), (2, 5), (4, 6), (6, 7), (5, 8)] result = max_activities(activities) print("Las actividades seleccionadas son:", result)
Explicación:
- Ordenar: Las actividades se ordenan por su tiempo de finalización.
- Seleccionar: Se selecciona la primera actividad y luego se seleccionan las siguientes actividades que no se solapen con las seleccionadas previamente.
Ejercicio 3: Programación Dinámica
Problema: Problema de la Mochila (0/1 Knapsack)
Dado un conjunto de elementos, cada uno con un peso y un valor, determina el número de cada elemento que se puede incluir en una colección para que el peso total sea menor o igual a un límite dado y el valor total sea lo más grande posible.
Ejemplo:
Entrada: pesos = [1, 3, 4, 5], valores = [1, 4, 5, 7], capacidad = 7 Salida: 9 Explicación: Se seleccionan los elementos con pesos 3 y 4, que tienen un valor total de 9.
Solución:
Usaremos programación dinámica para resolver este problema.
Paso 1: Definir la DP
Definimos dp[i][w] como el valor máximo que se puede obtener usando los primeros i elementos con un peso máximo de w.
Paso 2: Recurrencia
Para cada elemento, tenemos dos opciones: incluirlo o no incluirlo.
Código:
def knapsack(valores, pesos, capacidad): n = len(valores) dp = [[0 for x in range(capacidad + 1)] for x in range(n + 1)] # Construye la tabla dp de abajo hacia arriba for i in range(n + 1): for w in range(capacidad + 1): if i == 0 or w == 0: dp[i][w] = 0 elif pesos[i-1] <= w: dp[i][w] = max(valores[i-1] + dp[i-1][w-pesos[i-1]], dp[i-1][w]) else: dp[i][w] = dp[i-1][w] return dp[n][capacidad] # Ejemplo de uso pesos = [1, 3, 4, 5] valores = [1, 4, 5, 7] capacidad = 7 result = knapsack(valores, pesos, capacidad) print("El valor máximo que se puede obtener es:", result)
Explicación:
- Tabla DP: Se construye una tabla dp donde dp[i][w] representa el valor máximo que se puede obtener con los primeros i elementos y un peso máximo de w.
- Recurrencia: Para cada elemento, se decide si incluirlo o no, y se actualiza la tabla dp en consecuencia.
Ejercicio 4: Backtracking
Problema: N-Reinas
Coloca N reinas en un tablero de ajedrez NxN de manera que ninguna reina pueda atacar a otra.
Ejemplo:
Solución:
Usaremos backtracking para resolver este problema.
Paso 1: Colocar Reinas
Intentamos colocar una reina en cada fila y verificamos si la posición es segura.
Paso 2: Recursión
Si la posición es segura, colocamos la reina y procedemos a la siguiente fila.
Paso 3: Retroceder
Si no se puede colocar una reina en ninguna columna de la fila actual, retrocedemos y movemos la reina de la fila anterior.
Código:
def is_safe(board, row, col, N): # Verifica la columna for i in range(row): if board[i][col] == 'Q': return False # Verifica la diagonal superior izquierda for i, j in zip(range(row, -1, -1), range(col, -1, -1)): if board[i][j] == 'Q': return False # Verifica la diagonal superior derecha for i, j in zip(range(row, -1, -1), range(col, N)): if board[i][j] == 'Q': return False return True def solve_n_queens_util(board, row, N, solutions): if row >= N: solutions.append(["".join(row) for row in board]) return for col in range(N): if is_safe(board, row, col, N): board[row][col] = 'Q' solve_n_queens_util(board, row + 1, N, solutions) board[row][col] = '.' def solve_n_queens(N): board = [['.' for _ in range(N)] for _ in range(N)] solutions = [] solve_n_queens_util(board, 0, N, solutions) return solutions # Ejemplo de uso N = 4 result = solve_n_queens(N) print("Las soluciones para el problema de las N-Reinas son:") for solution in result: for row in solution: print(row) print()
Explicación:
- is_safe: Verifica si es seguro colocar una reina en una posición dada.
- solve_n_queens_util: Utiliza backtracking para colocar reinas en el tablero.
- solve_n_queens: Inicializa el tablero y llama a la función recursiva para encontrar todas las soluciones.
Conclusión
En esta sección, hemos practicado diversas estrategias de diseño de algoritmos mediante ejercicios prácticos. Hemos cubierto técnicas de "Divide y Vencerás", "Algoritmos Greedy", "Programación Dinámica" y "Backtracking". Cada ejercicio ha sido acompañado de una solución detallada y explicaciones paso a paso para asegurar una comprensión profunda de los conceptos.
En el siguiente módulo, nos enfocaremos en la optimización de algoritmos, donde aprenderemos técnicas para mejorar el rendimiento y la eficiencia de nuestros códigos.
Curso de Análisis y Diseño de Algoritmos
Módulo 1: Introducción a los Algoritmos
Módulo 2: Análisis de Algoritmos
- Análisis de Complejidad Temporal
- Análisis de Complejidad Espacial
- Casos de Complejidad: Mejor, Peor y Promedio
Módulo 3: Estrategias de Diseño de Algoritmos
Módulo 4: Algoritmos Clásicos
- Búsqueda Binaria
- Ordenamiento por Inserción
- Ordenamiento por Mezcla (Merge Sort)
- Ordenamiento Rápido (Quick Sort)
- Algoritmo de Dijkstra
- Algoritmo de Floyd-Warshall