Graph · 14 of 43
Minimum time taken by each job to be completed given by a Directed Acyclic Graph
Each job takes one minute and some jobs must wait for others, so find the earliest minute every job can be finished.
- Simple way
- time O(V * E)
room O(V) - Clever way
- time O(V + E)
room O(V + E)
The problem, simply
You have V jobs. Every job takes exactly 1 minute. Some arrows say "this job must be finished before that job can start". The arrows never go round in a circle, so the jobs can always be done.
As many jobs as you like can run at the same time. For every job, give back the earliest minute at which it can be finished.
A story to remember it
Picture it
Seven jobs. An arrow from 1 to 3 means job 3 waits for job 1. Jobs 0 and 1 wait for nobody. Job 6 waits for 1, 4 and 5.
- just added
Examples
The cake jobs
- In
V = 7, edges = 0>2, 1>2, 1>3, 2>4, 3>4, 3>5, 4>6, 5>6, 1>6- Out
[1, 1, 2, 2, 3, 3, 4]
Job 6 waits for job 1 (done at minute 1) and for jobs 4 and 5 (done at minute 3). It has to wait for the slowest, so it finishes at minute 4.
One long chain
- In
V = 6, edges = 0>1, 0>2, 1>3, 2>3, 3>4, 4>5- Out
[1, 2, 2, 3, 4, 5]
Jobs 1 and 2 run side by side in minute 2. After that it is a single line, one job per minute.
No arrows at all
- In
V = 4, edges = none- Out
[1, 1, 1, 1]
Nobody waits for anybody, so everything finishes in the first minute.
One job
- In
V = 1, edges = none- Out
[1]
A single job is done after 1 minute, not 0.
Way 1: the simple way
Give every job the number 1. Then keep walking over all the arrows and fix any job that finished too early.
- Set
time[v] = 1for every job. - Look at every arrow
u > v. Iftime[u] + 1is bigger thantime[v], raisetime[v]to it. - Repeat step 2 until a full pass changes nothing. The longest chain has at most
Vjobs, so at mostVpasses are needed.
class JobTimesRounds {
public static int[] minTime(int n, int[][] edges) {
int[] time = new int[n];
Arrays.fill(time, 1); // every job takes at least 1 minute
for (int round = 0; round < n; round++) {
boolean changed = false;
for (int[] e : edges) {
int u = e[0], v = e[1];
if (time[u] + 1 > time[v]) { // v cannot finish before u does
time[v] = time[u] + 1;
changed = true;
}
}
if (!changed) break; // nothing moved, all times are final
}
return time;
}
}How long it takes: up to V passes over E arrows, O(V * E). Extra room: only the time array, O(V). It is correct, but it keeps looking at arrows whose jobs are already settled.
Way 2: the clever way
Use Mom's sticky notes. Handle a job only after all the jobs it waits for are done, so each job is handled once and each arrow is looked at once. Doing jobs in an order where every arrow points forward is called a topological order, and the sticky note count is the in-degree.
- Count, for every job, how many arrows point into it. That is its sticky note.
- Every job whose note says 0 finishes at minute 1. Put it in a waiting line (a queue).
- Take a job
ufrom the front. For each arrowu > v: settime[v] = max(time[v], time[u] + 1), and cross one offv's note. - When
v's note reaches 0, all its jobs are done, sotime[v]is final. Putvin the line.
Sticky notes count down, finish times flow along the arrows
01 / 08
Count arrows into each job. Jobs 0 and 1 have nothing to wait for, so they finish at minute 1 and join the line.
- looking now
- checking
- done
- answer
- just added
class JobTimesKahn {
public static int[] minTime(int n, int[][] edges) {
List<List<Integer>> next = new ArrayList<>();
for (int i = 0; i < n; i++) next.add(new ArrayList<>());
int[] waiting = new int[n]; // the sticky note
for (int[] e : edges) {
next.get(e[0]).add(e[1]);
waiting[e[1]]++;
}
int[] time = new int[n];
Queue<Integer> ready = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (waiting[i] == 0) { time[i] = 1; ready.add(i); }
}
while (!ready.isEmpty()) {
int u = ready.poll(); // time[u] is final now
for (int v : next.get(u)) {
time[v] = Math.max(time[v], time[u] + 1);
if (--waiting[v] == 0) ready.add(v); // all of v's jobs are done
}
}
return time;
}
}How long it takes: each job joins the line once and each arrow is followed once, O(V + E). Extra room: the arrow lists, the notes and the line, O(V + E).
Watch out for
- Jobs numbered from 1. Many judges use 1 to N. Make arrays of size
N + 1or subtract 1. - A job with no arrows in finishes at minute 1, not 0.
- If the arrows had a circle, some notes would never reach 0. Count how many jobs left the line; if it is less than
V, report a cycle. - Reading the size of the line level by level also works: everything in level
kfinishes at minutek. Both give the same answer.
What interviewers ask next
- Jobs take different amounts of time. Use
time[v] = max(time[v], time[u] + dur[v]), starting withtime[v] = dur[v]. The largest value is the length of the critical path. - What is the earliest the whole project ends? The largest finish time, which is also the number of jobs on the longest chain.
- You only have
kworkers. Now it is much harder. For smallV, use dynamic programming over subsets of finished jobs (LeetCode "Parallel Courses II"). - Can you do it with depth first search? Yes.
time[v] = 1 + max(time[u])over the jobsvwaits for, with memo so each job is solved once. SameO(V + E).
Remember this
Grown-up words: the jobs and arrows form a directed acyclic graph (DAG), the sticky note is the in-degree, the order the jobs leave the line is a topological order (Kahn's algorithm), and the finish time is the longest path ending at that job.