Games & Miscellaneous contents
Scheduling Jobs on One Machine
Order jobs on a single machine to minimize the total waiting penalty; the permutation (adjacent swap) method gives a sorting solution for linear, exponential and identical penalty functions.
Read first: Lists and Tuples
We have jobs and a single machine. Job takes time to process, and waiting for time units before its processing starts costs a penalty . Find an order (permutation ) of the jobs that minimizes the total penalty:
In general the problem is hard; there are three special cases with a simple sorting solution, all derived by the permutation method: swap two adjacent jobs in an optimal schedule, compute the change in the penalty, and read off the condition that no swap may improve the schedule.
def total_penalty(order, times, penalty):
"""order: job indices; times[i]: processing time; penalty(i, t): the penalty of job i waiting t."""
total, elapsed = 0, 0
for job in order:
total += penalty(job, elapsed)
elapsed += times[job]
return totalLinear penalties
Let with (a constant term could be summed up separately and dropped).
Take a schedule and swap the jobs at positions and . Only these two summands change, and the difference simplifies to
In an optimal schedule this cannot be negative for any :
So the optimal schedule is obtained by sorting the jobs by in non-increasing order: do first the jobs that are costly to delay and short to process. (This is the classic "Smith's rule".)
from fractions import Fraction
def schedule_linear(times, costs):
"""Jobs sorted by c/t, largest first (exact, with fractions)."""
return sorted(range(len(times)), key=lambda i: -Fraction(costs[i], times[i]))
times, costs = [3, 1, 2], [3, 5, 1]
order = schedule_linear(times, costs)
assert order == [1, 0, 2]
assert total_penalty(order, times, lambda i, t: costs[i] * t) == 5 * 0 + 3 * 1 + 1 * 4Exponential penalties
Let with and . The same argument shows that the jobs should be sorted in non-increasing order of
(a job with never costs anything and can go last).
import math
def schedule_exponential(times, costs, alpha):
def key(i):
return -(1 - math.exp(alpha * times[i])) / costs[i] if costs[i] > 0 else float("-inf")
return sorted(range(len(times)), key=key)Identical monotone penalty
If all are the same non-decreasing function, then it is best to start with the shortest jobs: sort by non-decreasing (each job's waiting time is then as small as possible).
def schedule_identical(times):
return sorted(range(len(times)), key=times.__getitem__)The Livshits–Kladov theorem
The permutation method (sorting by a key) works only for these three cases, in the following sense: under the assumption of smooth penalty functions, the theorem says that the penalty functions must be
- linear: with ;
- exponential: with ;
- identical: , a monotone increasing function.
For any other family of penalty functions, no such comparison-based rule exists in general. All three solutions take .
Testing against all permutations
Each rule is compared with the best permutation found by exhaustive search (small ):
import random
from itertools import permutations
def check_linear(times, costs):
linear = lambda i, t: costs[i] * t
best = min(total_penalty(p, times, linear) for p in permutations(range(len(times))))
assert total_penalty(schedule_linear(times, costs), times, linear) == best
def check_exponential(times, costs, alpha):
expo = lambda i, t: costs[i] * math.exp(alpha * t)
best = min(total_penalty(p, times, expo) for p in permutations(range(len(times))))
got = total_penalty(schedule_exponential(times, costs, alpha), times, expo)
assert abs(got - best) <= 1e-9 * max(1.0, best)
def check_identical(times, phi):
same = lambda i, t: phi(t)
best = min(total_penalty(p, times, same) for p in permutations(range(len(times))))
assert total_penalty(schedule_identical(times), times, same) == best
rnd = random.Random(1)
for _ in range(300):
n = rnd.randint(1, 6)
times = [rnd.randint(1, 6) for _ in range(n)]
check_linear(times, [rnd.randint(0, 9) for _ in range(n)])
check_exponential(times, [rnd.randint(1, 9) for _ in range(n)], rnd.choice([0.1, 0.5, 1.0]))
check_identical(times, lambda t: t * t + 3 * t)
check_identical(times, lambda t: math.sqrt(t))The permutation method in general
The recipe to solve a scheduling problem by exchange arguments:
- Write the cost of a schedule, and compare it with the cost after swapping two adjacent jobs; only the two swapped terms change.
- Turn "the swap does not help" into an inequality between the two jobs that does not involve any other job, like .
- If this inequality defines a consistent total order (a key), sorting by it gives the optimal schedule; adjacent swaps can then bubble any permutation into the sorted one without ever improving.
The same method gives Johnson's rule for two machines.