📚 Math in Society
⇩ Download ▾

7.4 Choosing a Priority List

We will explore two algorithms for selecting a priority list.

Decreasing time algorithm

The decreasing time algorithm takes the approach of trying to get the very long tasks out of the way as soon as possible by putting them first on the priority list.

Using the decreasing time algorithm, the priority list led to a schedule with a finishing time of 35. Is this good? It certainly looks like there was a lot of idle time in this schedule. To get some idea how good or bad this schedule is, we could compute the critical time, the minimum time to complete the job. To find this, we look for the sequence of tasks with the highest total completion time. For this digraph that sequence would appear to be: T2,T6,T5,T8,T10, with total sequence time of 28. From this we can conclude that our schedule isn’t horrible, but there is a possibility that a better schedule exists.

Critical path algorithm

A sequence of tasks in the digraph is called a path. In the previous example, we saw that the critical path dictates the minimum completion time for a schedule. Perhaps, then, it would make sense to consider the critical path when creating our schedule. For example, in the last schedule, the processors began working on tasks 1 and 3 because they were longer tasks, but starting on task 2 earlier would have allowed work to begin on the long task 6 earlier.

The critical path algorithm allows you to create a priority list based on idea of critical paths.

I’m sure you can imagine that searching for the critical path every time you remove a task from the digraph would get really tiring, especially for a large digraph. In practice, the critical path algorithm is implementing by first working from the end backwards. This is called the backflow algorithm.

# Ch 7.4 - the backflow algorithm. Work backwards from the end of the digraph
# and label every task with its critical time: how long the job still has to run
# once that task starts.
TASKS = {                       # task: (time, prerequisites)
    "T1": (6, []),      "T2": (3, []),          "T3": (7, []),
    "T4": (4, []),      "T5": (5, ["T1", "T6"]), "T6": (10, ["T2"]),
    "T7": (4, ["T3", "T6"]), "T8": (3, ["T5", "T7"]),  "T9": (2, ["T5"]),
    "T10": (7, ["T5", "T7", "T8"]),
}
# TRY IT: change T6 from 10 to 2 and re-run. The critical path moves.

successors = {t: [s for s in TASKS if t in TASKS[s][1]] for t in TASKS}
critical, order = {}, []
while len(critical) < len(TASKS):
    for t in TASKS:
        if t in critical or any(s not in critical for s in successors[t]):
            continue
        critical[t] = TASKS[t][0] + max([critical[s] for s in successors[t]] + [0])
        order.append(t)

print("backflow, in the order the labels can be filled in:")
for t in order:
    downstream = successors[t]
    via = (f"{TASKS[t][0]} + {max(critical[s] for s in downstream)} "
           f"(through {max(downstream, key=lambda s: critical[s])})"
           if downstream else f"{TASKS[t][0]} + 0 (it feeds the end)")
    print(f"   critical time of {t:<4} = {via:<28} = {critical[t]}")

start = max(critical, key=lambda t: critical[t])
path, cur = [], start
while cur:
    path.append(cur)
    nxt = successors[cur]
    cur = max(nxt, key=lambda s: critical[s]) if nxt else None
print(f"\ncritical path: {' -> '.join(path)}")
print(f"critical time: {' + '.join(str(TASKS[t][0]) for t in path)} = {critical[start]}")
print(f"total work if one person did it all: {sum(t for t, _ in TASKS.values())}\n")

by_critical = sorted(TASKS, key=lambda t: (-critical[t], int(t[1:])))
by_time = sorted(TASKS, key=lambda t: (-TASKS[t][0], int(t[1:])))
print("critical path priority list :", ", ".join(by_critical))
print("decreasing time priority list:", ", ".join(by_time))
print("\nThe two lists disagree because a short task can still be the gate that")
print("holds up a very long chain behind it. Look at T2: only 3 units of work,")
print(f"but critical time {critical['T2']} - nothing else can finish until it does.")

One you have completed the backflow algorithm, you can easily create the critical path priority list by using the critical times you just found.

This version of the Critical Path Algorithm will usually be the easier to implement.

By observation, we can see that a much better schedule exists for the example above:

A better three processor schedule for the same tasks, boundary times 1, 3 and 9. P sub 1 runs T sub 1 from 0 to 1, T sub 6 from 1 to 3 and T sub 9 from 3 to 9. P sub 2 runs T sub 2 from 0 to 1, T sub 7 from 1 to 3 and T sub 5 from 3 to 9. P sub 3 runs T sub 3 from 0 to 1, T sub 8 from 1 to 3 and T sub 4 from 3 to 9. No processor is ever idle and the finishing time is 9.

In most cases the critical path algorithm will lead to a very good schedule. There are cases, like this, where it will not. Unfortunately, there is no known algorithm to always produce the optimal schedule.

# Ch 7.4 - decreasing time vs critical path, scheduled on N processors.
TASKS = {
    "T1": (6, []),      "T2": (3, []),           "T3": (7, []),
    "T4": (4, []),      "T5": (5, ["T1", "T6"]), "T6": (10, ["T2"]),
    "T7": (4, ["T3", "T6"]), "T8": (3, ["T5", "T7"]),  "T9": (2, ["T5"]),
    "T10": (7, ["T5", "T7", "T8"]),
}
PROCESSORS = 2      # TRY IT: 3, then 4. Does the critical path list stay ahead?

successors = {t: [s for s in TASKS if t in TASKS[s][1]] for t in TASKS}
critical = {}
while len(critical) < len(TASKS):
    for t in TASKS:
        if t not in critical and all(s in critical for s in successors[t]):
            critical[t] = TASKS[t][0] + max([critical[s] for s in successors[t]] + [0])

def schedule(procs, priority):
    done, running, now, log = {}, {}, 0, []
    while len(done) < len(TASKS):
        for p in range(procs):
            if p in running:
                continue
            live = [t for t, _ in running.values()]
            ready = [t for t in priority if t not in done and t not in live
                     and all(q in done for q in TASKS[t][1])]
            if ready:
                running[p] = (ready[0], now + TASKS[ready[0]][0])
                log.append((now, now + TASKS[ready[0]][0], p, ready[0]))
        now = min(f for _, f in running.values())
        for p in [p for p, (t, f) in running.items() if f <= now]:
            done[running.pop(p)[0]] = now
    return max(done.values()), log

def gantt(log, finish, procs, width=56):
    for p in range(procs):
        bar = [" "] * width
        for start, stop, q, t in log:
            if q == p:
                a = int(round(start / finish * width))
                b = max(a + 1, int(round(stop / finish * width)))
                bar[a:b] = list(t.center(b - a, "-")[:b - a])
        print(f"   P{p + 1} |{''.join(bar)}|")

work = sum(t for t, _ in TASKS.values())
best_possible = max(critical.values())
print(f"{len(TASKS)} tasks, {work} units of work, critical time {best_possible}.")
print(f"On {PROCESSORS} processors nothing can finish sooner than "
      f"max({best_possible}, {work}/{PROCESSORS} = {work / PROCESSORS:g}).\n")
lists = {
    "decreasing time": sorted(TASKS, key=lambda t: (-TASKS[t][0], int(t[1:]))),
    "critical path":   sorted(TASKS, key=lambda t: (-critical[t], int(t[1:]))),
}
for name, priority in lists.items():
    finish, log = schedule(PROCESSORS, priority)
    print(f"{name}: {', '.join(priority)}")
    print(f"   finishing time {finish}, idle time {PROCESSORS * finish - work}"
          + ("   <-- hits the critical time, so it is optimal"
             if finish == best_possible else ""))
    gantt(log, finish, PROCESSORS)
    print()
print("Getting the long jobs started early is not the same as getting the")
print("BLOCKING jobs started early, and only the second one is what matters.")

Adapted from Math in Society by David Lippman, hosted on LibreTexts (math.libretexts.org) and licensed under CC BY-SA 3.0. Changes were made. License: CC-BY-SA-3.0.

These eBooks are a prerelease and are not yet certified conformant with WCAG 2.1 AA or ADA Title II. Every page is built against an automated accessibility gate, and the published editions will meet ADA Title II requirements when they release in late September 2026. If something is unusable, please tell us.