Skip to Content

Planificar trabajos en dos máquinas

Esta tarea trata de hallar un plan óptimo para nn trabajos en dos máquinas. Cada ítem debe procesarse primero en la primera máquina, y después en la segunda. El ii-ésimo trabajo toma tiempo aia_i en la primera máquina, y tiempo bib_i en la segunda. Cada máquina solo puede procesar un trabajo a la vez.

Queremos hallar el orden óptimo de los trabajos, de modo que el tiempo de procesamiento final sea el mínimo posible.

Esta solución que se discute aquí se llama regla de Johnson (por S. M. Johnson).

Cabe señalar que la tarea se vuelve NP-completa si tenemos más de dos máquinas.

Construcción del algoritmo

Nótese primero que podemos asumir que el orden de los trabajos para la primera y la segunda máquina tiene que coincidir. De hecho, como los trabajos para la segunda máquina se vuelven disponibles después de procesarlos en la primera, y si hay varios trabajos disponibles para la segunda máquina, entonces el tiempo de procesamiento será igual a la suma de sus bib_i, independientemente de su orden. Por lo tanto solo es ventajoso enviar los trabajos a la segunda máquina en el mismo orden en que los enviamos a la primera.

Consideremos el orden de los trabajos, que coincide con su orden de entrada 1,2,,n1, 2, \dots, n.

Denotamos por xix_i el tiempo ocioso de la segunda máquina inmediatamente antes de procesar ii. Nuestro objetivo es minimizar el tiempo ocioso total:

F(x)=xi minF(x) = \sum x_i ~ \rightarrow \min

Para el primer trabajo tenemos x1=a1x_1 = a_1. Para el segundo trabajo, como se envía a la máquina en el tiempo a1+a2a_1 + a_2, y la segunda máquina se libera en x1+b1x_1 + b_1, tenemos x2=max((a1+a2)(x1+b1),0)x_2 = \max\left((a_1 + a_2) - (x_1 + b_1), 0\right). En general obtenemos la ecuación:

xk=max(i=1kaii=1k1bii=1k1xi,0)x_k = \max\left(\sum_{i=1}^k a_i - \sum_{i=1}^{k-1} b_i - \sum_{i=1}^{k-1} x_i, 0 \right)

Ahora podemos calcular el tiempo ocioso total F(x)F(x). Se afirma que tiene la forma

F(x)=maxk=1nKi,F(x) = \max_{k=1 \dots n} K_i,

donde

Ki=i=1kaii=1k1bi.K_i = \sum_{i=1}^k a_i - \sum_{i=1}^{k-1} b_i.

Esto se puede verificar fácilmente por inducción.

Ahora usamos el método de permutaciones: intercambiaremos dos trabajos vecinos jj y j+1j+1 y veremos cómo esto cambiará el tiempo ocioso total.

Por la forma de la expresión de KiK_i, está claro que solo cambian KjK_j y Kj+1K_{j+1}; denotamos sus nuevos valores con KjK_j’ y Kj+1K_{j+1}‘.

Si este cambio de los trabajos jj y j+1j+1 aumentó el tiempo ocioso total, tiene que ser el caso que:

max(Kj,Kj+1)max(Kj,Kj+1)\max(K_j, K_{j+1}) \le \max(K_j’, K_{j+1}‘)

(Intercambiar dos trabajos también podría no tener ningún impacto. La condición de arriba es solo suficiente, pero no necesaria.)

Después de quitar i=1j+1aii=1j1bi\sum_{i=1}^{j+1} a_i - \sum_{i=1}^{j-1} b_i de ambos lados de la desigualdad, obtenemos:

max(aj+1,bj)max(bj+1,aj)\max(-a_{j+1}, -b_j) \le \max(-b_{j+1}, -a_j)

Y después de quitar los signos negativos:

min(aj,bj+1)min(bj,aj+1)\min(a_j, b_{j+1}) \le \min(b_j, a_{j+1})

Así obtuvimos un comparador: al ordenar los trabajos con él, obtenemos un orden óptimo de los trabajos, en el que ningún par de trabajos se puede intercambiar con una mejora del tiempo final.

Sin embargo se puede simplificar aún más el ordenamiento, si se mira el comparador desde otro ángulo. El comparador se puede interpretar de la siguiente manera: si tenemos los cuatro tiempos (aj,aj+1,bj,bj+1)(a_j, a_{j+1}, b_j, b_{j+1}), y el mínimo de ellos es un tiempo correspondiente a la primera máquina, entonces el trabajo correspondiente debería hacerse primero. Si el tiempo mínimo es un tiempo de la segunda máquina, entonces debería ir después. Así podemos ordenar los trabajos por min(ai,bi)\min(a_i, b_i), y si el tiempo de procesamiento del trabajo actual en la primera máquina es menor que el tiempo de procesamiento en la segunda, entonces este trabajo debe hacerse antes que todos los trabajos restantes, y en caso contrario después de todas las tareas restantes.

De una u otra forma, resulta que por la regla de Johnson podemos resolver el problema ordenando los trabajos, y así recibir una complejidad temporal de O(nlogn)O(n \log n).

Implementación

Aquí implementamos la segunda variación del algoritmo descrito.

struct Job { int a, b, idx; bool operator<(Job o) const { return min(a, b) < min(o.a, o.b); } }; vector<Job> johnsons_rule(vector<Job> jobs) { sort(jobs.begin(), jobs.end()); vector<Job> a, b; for (Job j : jobs) { if (j.a < j.b) a.push_back(j); else b.push_back(j); } a.insert(a.end(), b.rbegin(), b.rend()); return a; } pair<int, int> finish_times(vector<Job> const& jobs) { int t1 = 0, t2 = 0; for (Job j : jobs) { t1 += j.a; t2 = max(t2, t1) + j.b; } return make_pair(t1, t2); }

Toda la información de cada trabajo se guarda en un struct. La primera función ordena todos los trabajos y computa el plan óptimo. La segunda función computa los tiempos de finalización de ambas máquinas dado un plan.