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
