Secuencia: por qué importa el orden

Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Preparar café instantáneo tiene una secuencia obligatoria y otra que no lo es. Hervir el agua antes de verterla sobre el café no se puede invertir sin arruinar el resultado: verter agua fría no disuelve nada. Agregar el azúcar antes o después de la leche, en cambio, no cambia nada: el café queda igual de dulce y de claro sin importar cuál de los dos se sirva primero. La primera es una dependencia real -un paso necesita algo que dejó el anterior-; la segunda es solo el orden en que alguien decidió escribir los pasos, no una condición del problema.
El post anterior ya usaba esta idea sin nombrarla. Descomponer el costo de un cumpleaños en costo_comida y costo_globos funcionaba porque esas dos partes no necesitaban ningún dato una de la otra: daba igual resolver primero una que la otra. Este post nombra esa propiedad y muestra qué pasa cuando deja de cumplirse.
Ejecutar un algoritmo en secuencia significa correr sus pasos uno detrás del otro, en un orden fijo. Eso es cierto de cualquier algoritmo, incluso de los que más adelante incorporen decisiones o repeticiones: en cada instante se ejecuta un solo paso.
Lo que varía, paso a paso, es si ese orden se puede cambiar sin alterar el resultado. La respuesta depende de una sola pregunta: ¿el paso B usa algo que el paso A calculó?
La mayoría de los algoritmos son una mezcla de las dos cosas: algunos pasos forman una cadena estricta, y entre esos bloques hay partes que no le deben nada a las demás. Reconocer cuál es cuál evita dos errores distintos: escribir un paso antes de que el dato que necesita exista, y asumir que un orden es obligatorio cuando en realidad no importa.
El siguiente algoritmo calcula el total de una compra con envío y un descuento sobre el subtotal.
algoritmo total_compra(precio_producto, cantidad, porcentaje_descuento, costo_envio):
multiplicar precio_producto por cantidad para obtener el subtotal
dividir porcentaje_descuento por 100
multiplicar el subtotal por ese resultado para obtener el descuento
restar el descuento al subtotal para obtener el subtotal con descuento
sumar el costo_envio al subtotal con descuento
devolver el resultadoCada paso, a partir del segundo, depende del anterior: el descuento se calcula sobre el subtotal, así que el subtotal tiene que estar resuelto antes; el envío se suma sobre el subtotal ya con descuento, así que ese paso va al final. Con un precio de $2500, 3 unidades, 10% de descuento y $500 de envío: el subtotal es $7500, el descuento $750, el subtotal con descuento $6750, y el total final $7250.
Ahora la misma operación, con un solo cambio: sumar el envío antes de calcular el descuento, en lugar de después.
algoritmo total_compra(precio_producto, cantidad, porcentaje_descuento, costo_envio):
multiplicar precio_producto por cantidad para obtener el subtotal
sumar el costo_envio al subtotal
dividir porcentaje_descuento por 100
multiplicar ese resultado por el subtotal con envío para obtener el descuento
restar el descuento
devolver el resultadoCon los mismos datos, el subtotal con envío queda en $8000, el 10% de descuento sobre ese monto es $800, y el total final es $7200: cincuenta pesos menos que el resultado correcto. El algoritmo no tiene ningún error de cálculo -cada paso hace exactamente lo que dice-, pero el descuento terminó aplicándose también sobre el envío, algo que el problema nunca pidió. Cambiar el orden de dos pasos que sí tenían una dependencia real cambió el resultado sin que nada avisara del error.
Calcular cuántos minutos toma armar un pedido en una fábrica que produce varios lotes iguales, sabiendo que antes de arrancar cada lote hace falta un tiempo fijo de preparación de la máquina.
Pista: hay una cadena de tres pasos, cada uno necesita el resultado del anterior. Antes de escribir nada, identificar qué dato falta para poder calcular cada cantidad: primero el tiempo de armado de un lote, después el tiempo total de ese lote con la preparación incluida, y recién ahí el tiempo de todos los lotes juntos.
algoritmo minutos_totales(piezas, piezas_por_minuto, minutos_preparacion, cantidad_lotes):
dividir piezas por piezas_por_minuto para obtener los minutos de armado por lote
sumar minutos_preparacion a los minutos de armado por lote para obtener los minutos por lote
multiplicar los minutos por lote por cantidad_lotes
devolver el resultadoCon 120 piezas por lote a 8 piezas por minuto, 10 minutos de preparación y 4 lotes: el armado de un lote toma 15 minutos, cada lote completo (con preparación) toma 25, y el total son 100 minutos. Ninguno de los tres pasos se puede adelantar: multiplicar por la cantidad de lotes antes de sumar la preparación multiplicaría también la preparación, y dividir después de sumar la preparación mezclaría un tiempo fijo con uno que depende de la cantidad de piezas.
Una receta rinde una cantidad de porciones con una cantidad fija de un ingrediente. Al escalarla a una cantidad distinta de porciones, calcular cuántos paquetes completos de ese ingrediente hace falta comprar, sabiendo que cada paquete trae una cantidad fija en gramos.
Pista: el dato que hace falta para escalar la receta -cuánto ingrediente corresponde a una sola porción- no está en el enunciado, hay que calcularlo primero a partir de la receta original. Recién con eso resuelto se puede calcular cuánto hace falta para la cantidad nueva de porciones, y con ese número, cuántos paquetes comprar.
algoritmo paquetes_necesarios(gramos_receta_original, porciones_originales, porciones_nuevas, gramos_por_paquete):
dividir gramos_receta_original por porciones_originales para obtener los gramos por porción
multiplicar los gramos por porción por porciones_nuevas para obtener los gramos necesarios
dividir los gramos necesarios por gramos_por_paquete
redondear ese resultado hacia arriba
devolver el resultadoCon una receta que usa 300 gramos para 4 porciones, escalada a 10 porciones, y paquetes de 1000 gramos: cada porción original usa 75 gramos, la receta escalada necesita 750 gramos, y 750 dividido 1000 da 0,75 paquetes, que redondeado hacia arriba son 1. Los cuatro pasos forman una cadena sin ninguna parte intercambiable: cada uno existe únicamente porque el anterior dejó el dato que necesita.
El orden de los pasos de un algoritmo no es una cuestión de estilo: importa exactamente en los pasos donde uno necesita un dato que otro produjo, y no importa en absoluto entre los pasos que no comparten nada. Confundir una dependencia real con una simple costumbre de escritura -o al revés, asumir que un orden es obligatorio cuando dos pasos son independientes- es una fuente de errores tan silenciosa como confundir una restricción con ruido.
Hasta acá, cada algoritmo de la serie ejecutó siempre la misma secuencia de pasos, sin importar los datos de entrada. El próximo paso de la serie trata qué pasa cuando eso deja de ser cierto: un algoritmo que, según el dato que recibe, elige entre más de un camino posible.