cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 108 of 218
Ideas this builds on
Thinking in DP108 / 218

Parallel Courses III

Picture

A and B can run together and take 2 and 5 hours. C needs both and takes 3 more. C finishes at max(2,5)+3=8. The graph tells us when an answer is ready; the DP tells us what answer to compute.

Predict

Why is adding all prerequisite durations wrong?

Walk through the small example

Independent prerequisite branches run concurrently. Only the slowest required chain delays the next course. Summing would count work that happens at the same time.

Mental model and complete Python solution

finish[v] is the earliest achievable completion time of v, including v’s own duration. Initialize it to that duration. For each processed edge u→v, offer finish[u]+duration[v]. Keep the maximum offer because every prerequisite must finish. Kahn’s incoming count releases v only after all offers arrive. By induction in topological order, each finalized finish value is both necessary and achievable with unlimited parallelism. The overall answer is the largest course finish.

What the function promises: n courses are labeled 1..n. Each relation [u,v] means finish u before v. durations is indexed from zero. Durations are nonnegative and workers are unlimited. Return minimum completion time, or None for an invalid cyclic prerequisite system.

from collections import deque

def minimum_course_time(n, relations, durations):
    graph = [[] for _ in range(n)]
    incoming = [0] * n
    for earlier, later in relations:
        u, v = earlier - 1, later - 1
        graph[u].append(v)
        incoming[v] += 1
    finish = list(durations)
    ready = deque(i for i in range(n) if incoming[i] == 0)
    processed = 0
    while ready:
        u = ready.popleft()
        processed += 1
        for v in graph[u]:
            finish[v] = max(finish[v], finish[u] + durations[v])
            incoming[v] -= 1
            if incoming[v] == 0:
                ready.append(v)
    return max(finish, default=0) if processed == n else None

assert minimum_course_time(3, [(1,3), (2,3)], [2,5,3]) == 8
assert minimum_course_time(2, [(1,2), (2,1)], [1,1]) is None
assert minimum_course_time(0, [], []) == 0

Work and storage: O(V+E) time and space, with no recursion. “Longest path” is tractable here because the dependency graph is acyclic, not because longest paths are easy on arbitrary graphs.

When to recognize it: A course waits for its slowest prerequisite chain, not the sum of independent chains.

Check before applying it: A topological order alone is not a duration. Limited workers would create a different scheduling problem.

Change one rule

Instead, count paths from a source to a target on a DAG. What remains, and what changes?

See what changes

Keep the topological order. Set ways[source]=1 and other counts to zero, then add ways[u] into ways[v] for each edge u→v. Use addition rather than maximum because distinct incoming paths are all counted. State meaning determines the combine operation.

Connect

Course Schedule II: Readiness gives the evaluation order.

Longest Increasing Path in a Matrix: A matrix can hide the same acyclic dependency structure.

Problem reference: Parallel Courses III (Hard). The walkthrough and Python solution above are original course material.