This lecture focuses on solving any all-pairs shortest paths (APSP) in weighted graphs. Johnson's algorithm is introduced.
Instructor: Jason Ku
JASON KU: Welcome, everybody, to our lecture 14 of 6.006. This is our last lecture on graph algorithms, in particular, the last lecture we'll have on weighted shortest paths. But we're going to talk about a slightly different problem today, different than single source shortest paths. We're going to be talking about all-pairs shortest paths.
But first, let's review our single source shortest paths algorithms. We had BFS, DAG relaxation, Dijkstra, which we saw last time, which gets pretty close to linear. V log V plus E is pretty close to linear. It's much closer to linear than the general algorithm we have for solving single source shortest paths, namely, Bellman-Ford, which is a little bit more like quadratic.
This is like the difference between-- for sparse graphs, this is like the difference between sorting, using insertion sort and n squared, and merge sort in N log N, for example. We're going to get actually quite a big bonus for large input sizes by using Dijkstra when we can.
Today, we're going to be focusing on this new problem called all-pairs shortest paths. It's not really complicated. Instead of having a single source, we are essentially wanting to, given an input-- this is our weighted graph, where we've got a graph V, E, and we've got a weight function from the edges to the integers.
This is our general weighted graph. We want our output to be something like, the shortest path distance from u to v for every u and v in our vortex set. That's what we want to return.
Now, there's one caveat here that, if there's a negative weight cycle in our graph-- in other words, if any of these delta u, v's is minus infinity, there's a negative rate cycle in our graph. So unless, I guess-- or abort if G contains a negative weight cycle. So we're not actually going to worry about negative weight cycles in today's class.
If we have a graph, it could have negative weights. These are any integers. It could include negative weight edges. But as long as all of our path distances are bounded from below, none of them are negative infinity, we don't have any negative weight cycles, then I want you to output all of these shortest path distances.
Now, in particular, this output could-- any of these outputs needs to have size theta of V squared. Because for every pair of vertices, I need to return to you a number, or infinity, or minus infinity or something like that. But we are not dealing with a case with minus infinity. The output could have size-- this is a theta here. It does have size V squared.
But in particular, it's at least V squared because I need to give a number for each pair of vertices. And so we couldn't hope for linear time in the size of this graph for this problem, right? Single source shortest paths, for certain versions of the problem, we need to read the graph. And so we need to use linear time.
But in this problem, our output has quadratic size in the number of vertices. So in a sense, we can't do better than this. We can't do better than quadratic. And actually, what's one way we could solve all-pairs shortest paths by using stuff we've already done in this class? That's why I put this slide up here.
Yeah, we could just solve a single source shortest paths algorithm from every vertex in my graph. That seems like a stupid thing to do. It's almost brute force on the vertices. But it's certainly a way we could solve this problem, in polynomial time.
And we could definitely solve it in order V squared E time, using Bellman-Ford. We just take V steps of Bellman-Ford and deal with a graph on any set of vertices. We can do better than this. We can do better than this for graphs that are special in some way. We can do V times V plus E, V times linear.
If our weights are positive and bounded, we can use BFS V times. Or if our graph doesn't have cycles, we could use DAG relaxation V times. Or if our graph had non-negative edge weights, we could get, basically, V squared log V plus V times E. And that's actually not bad.
In sparse graphs, this is what Bellman-Ford would give us. But if we had Dijkstra's, for example, if we had all positive edge weights-- or non-negative, sorry, we could get V squared log V plus V, E time. This is V times Dijkstra.
OK, so how do these running times compare? This is V times Bellman-Ford. This is V times Dijkstra. Let's just get a feel for this separation here. If we had a sparse graph where V is upper-bounded by the number of vertices, this one looks like V squared log V.
This one looks like V cubed. And we need to spend at least V squared time. So actually, this is really close to linear in the size of the graph, just off by a log factor, just like sorting would entail. And this one would have a linear factor. In the sparse graph, this would be a linear factor worse than this, instead of a logarithmic factor-- again, this linear to log separation.
We don't want to have to do this running time if we don't have to. That's the name of the game. And really, all we're going to do in this lecture is try to solve how we can make this running time faster by doing something a little bit more intelligent than running a single source shortest path algorithm from every vertex.
How are we going to do that? Well, we could-- let's see. What are we doing? Right. The idea here, if we had a graph-- should my graph be directed or undirected? I'm not sure. Let's see if we can make a directed graph. OK, so here's a directed graph.
Why do I not care about undirected graphs? Can anyone tell me? Yeah, it's because-- I don't care about undirected graphs because, if I had an undirected graph, I could detect whether I had negative weight cycles in constant time-- I'm sorry, in linear time. I could just check each edge, see if it has negative weight, because a negative weight edge, an undirected edge is a cycle of negative weight.
So I could just-- if it has any negative edge weights, I could return in linear time that it does, and I can abort. Or it has only positive weights, and I can still use Dijkstra. So that's all good. So we're only concerned about needing to run Bellman-Ford on directed graphs that potentially have negative edge weight.
OK, so here's a graph. Let's see. Is this a graph that I want? Sure. Let's say we've got that direction and this direction. Say we have a directed graph like this. And let's say this is s. This is our source. And we have weights being 2-- sorry, weights being 4, 1, 1, 2, 2, 2, 2.
So this is an example of a graph we might want to run all-pairs shortest paths on. Maybe we also have negative weights in this graph. In particular, this has a negative weight cycle. I don't want negative weight cycles, so I'm going to make this 0. So this graph doesn't have negative weight cycles. Great. That's true, great.
All right, so here's an example that we might want to compute shortest paths on. There's no s in all-pairs shortest paths. But I'm going to be talking about a couple of shortest paths from s in my next argument, so I'm just labeling that vertex as s.
OK, the claim-- the approach we're going to do, we're going to try to take a graph that has negative edge weights, directed graph. We don't know if it has negative cycles or not yet. But we want to compute all-pairs shortest paths, not in this running time, but in this running time.
How could we do that? Well, maybe it's possible that we can change the weight of every edge so that they're all positive, but shortest paths are preserved. So basically, if a particular path-- like OK, the shortest path from s to t here is 1, 2, 3.
I could change edge weights in this graph. Say, for example, if I changed 1 to 0 here, that would still make this a shortest path. I haven't done-- I've reweighted the graph. Shortest paths have to be the same in this graph.
But now-- sorry. Yeah, this is not a shortest path. OK, I'll make that minus 2, and then these both 2, and I think this 4. Man, I really should have done my example beforehand.
OK, so this still doesn't have negative weight cycles. It has a negative weight edge. But this path is longer than this path. So when this was 1, this had length of 3, which was shorter than this path. That is length 4. OK, cool. So this is the shortest path from s to t.
I could change weights in this graph, for example, changing 2 to 3 and 1 to 0. That changed weights in my graph, but shortest paths remain the same. So maybe there's a way I could reweight my edges so that shortest paths stay the same, shortest paths are preserved. So I'm going to put this back to where it was.
So idea-- make edge weights non-negative while preserving shortest paths. In other words, just reweight the edges here so that shortest paths in G-- this is G-- after we reweight will go to some graph G prime, with the same combinatorial structure just different edge weights. And we want shortest paths-- we want these weights to all be non-negative. And we want shortest paths, if there's a shortest path in G, it continues to be a shortest path in the reweighted graph.
That's the goal for today. If we can do that transformation and this is not negative, then we're done. We're done because we can just run Dijkstra V times on that new graph, get the shortest path distances, construct a shortest path tree from those distances, and then traverse that tree in the original graph, and compute shortest paths along that tree. So that's the claim.
Claim-- we can compute distances in G-- so we're going to restrict G prime to have with non-negative edge weights. If we have such a G prime with non-negative edge weights, we can compute distances in G from distances in G prime in V times V plus E, which is smaller than our Dijkstra running time. This is our Dijkstra running time. And this is smaller than that.
So that would be fine, right? What do we do? We have our new graph G prime. It has all positive edge weights, so we run all-pairs shortest paths in here by just running Dijkstra V times. And then for each vertex s, we have some shortest path tree to all the things that it's connected to.
We can look at that path in G. This is the same common [INAUDIBLE] graph, just with different edge weights. We can look at that same tree. I'm not going to be able to draw the exact same tree here. But what I can do is I can just BFS or DFS along this tree.
And every time I have an edge, because each one of these is the shortest path in this tree, I can just compute every time I traverse an edge what its shortest path distance is in G in linear time for that vertex for that particular s. I do that over all s's, and I get this running time. So that's the goal here.
All right, so we first wanted to find-- we were wanting to make edge weights non-negative while preserving shortest paths. Because if we could do that, we could solve our original all-pairs shortest paths problem. So this is the proof sketch. But how can we do this?
But how? It doesn't even seem like this could be possible, generally. How can I just reweight the edges and maintain that the shortest path trees are the same? That seems hard to do. And in particular, if there's a negative weight cycle in this graph, it's impossible to do.
So claim-- not possible if G contains a negative weight cycle. OK, the exclamation point is just my comment here. So if G contains a negative weight cycle, then in particular, the shortest path distance or a shortest path in G-- if this is G, say we have a negative weight cycle directed cycle C here.
In particular, the shortest path from this vertex on the cycle to this vertex on the cycle, what is its shortest path? It's infinite. A shortest path is infinite around this cycle. You just keep going around the cycle over and over and over again because it has a negative weight. Weight of C is less than 0, strictly. That's what a negative weight cycle is.
OK, so the shortest path-- a shortest path from s to t has infinite length and, in particular, is non-simple. However-- so shortest path from s to t is non-simple. But as we proved in the last lecture, shortest paths in a graph with non-negative weights are what? Are simple, right? Because they're just shortest path trees. So that's a contradiction. So this is not possible.
So given a graph with negative weights but no negative cycle, it's still not clear how we could find such a reweighting of the graph. Can we do this? Well, we're going to exploit a little idea here. How can we transform the weights of a path?
Well, how-- what's a-- a silly idea, I have this silly idea. If I don't want negative edge weights in this graph-- ugh, this is messy in the back. You got edge weights 1, minus 2, 0, 1, 4, 5, and 1. There's only one negative edge weight here.
What if I just added a large number, or in particular, the negative of the smallest edge in my graph to every edge in my graph? Then I'll have a graph with non-negative weights. Fantastic. Why is that not a good idea?
Well, in particular, if I did that to this graph, if I added 2 to every edge, the weight of this path, which was the shortest path, changed from weight 3 to weight 9, because I added 2 for every edge. But this path, which wasn't a shortest path in the original graph-- it had weight 4-- increased only by 2. Now that is a shortest path. Or it's a shorter path than this one, so this one can't be a shortest path.
So that transformation, sure, would make all the weights non-negative, but would not preserve shortest paths. In particular, if I added the same edge weight to every edge, I will bias toward taking paths that have fewer edges, not just smaller weight. So that first idea doesn't work.
Idea-- add large number to each edge. This is bad. Makes weights non-negative, but does not preserve shortest paths. So this is not a good idea-- bad idea. Is there any way you can think of to modify the edge weights in a graph in any way that will preserve shortest paths?
So here is an idea for you, which is kind of this critical step in Johnson's and in a lot of graph transformation algorithms. If I have a vertex, say this middle guy, say v, every path from v goes through an outgoing edge of v. And every path going into v goes through an edge going into v.
I haven't said anything-- I've said very stupid things. But that observation is critical here. If I add a number-- or let me see if I got this right in terms of adding and subtracting. If I add a number to all outgoing edges from a vertex, and I subtract that same number from the weights of all of the incoming edges to that vertex, then every path from v is changed by the same amount, because every path from v goes through one of those outgoing edges.
And any path going into v has also changed by the same amount. In particular, it's changed by a negative, whatever we added to the outgoing edges. So such a transformation, adding a number from all the outgoing edges from a vertex and subtracting that same number from all the incoming edges, preserves shortest paths. That's a claim.
Idea-- this is a better idea. Given vertex v, add-- I'm going to put this on two lines. Add weight h to all outgoing edges, and subtract weight to all incoming edges. So that's the idea. And the claim is, this transformation, shortest paths are preserved under this transformation.
And why is that? It's kind of the exact same argument that I had over there. Proof-- consider any path in my graph, either if the path-- path could go through v many times, or it could go through not at all. If my path, if I have a path, in my original graph G, then with path weight w of pi-- this is my path-- it goes through v some number of times.
So I'm going to say this is going from s to t. If it crosses v-- if it never crosses v, if it never touches v, the vertex that I transformed, then I argue that the path weight is the same because I didn't do anything to edges that are in this path. Alternatively, this thing goes through v sometimes. If it goes through v in the middle, how is the weight of my path changed?
Well, it hasn't, because I added a number to all outgoing edges, so there's an outgoing edge here with weight I've changed by weight h. And there's an incoming edge here that I've changed by weight negative h. So these cancel out and you've got 0. So passing through a vertex doesn't change the weight of my path.
The only way I could change the weight of my path is if v is the start vertex or the end vertex. So it's possible that s is my vertex or t is my vertex. Well, for any path leaving v, I will have increased the weight of that path by h, because I added a weight h to all outgoing edges.
So again, while the path weight has changed, since all of the paths leaving v have changed by the same amount, a shortest path will still be a shortest path. And same goes for t. If t, the end vertex is v, I'll have subtracted h from all of my incoming edges, which means that any path ending at t, any directed path ending at t, also changes by the same value. And so shortest paths must be preserved. So shortest paths preserved.
So that's pretty cool transformation. I can assign for any vertex such a transformation which affects all of the edges surrounding it by this h additive factor, either added or subtracted. So maybe-- and I can do this independently for every vertex. The shortest paths were preserved by me doing this to one vertex. Then if I do it to another vertex, then shortest paths are still preserved. And let's prove that real quick.
What I'm going to do is I'm going to want to do this to give me flexibility for changing all the edge weights in my graph to have this property. I'm going to set-- or define a potential function h that maps vertices to integers. So this is the potential h of v. And then we're going to make a graph, G prime based on above transformation for each vertex in v.
So I'm going to set a number, an h for each vertex. These are independent now. And I'm going to add that potential to all outgoing edges. And I'm going to subtract that potential from all incoming edges. This transformation is going to preserve shortest paths. Let's actually be a little bit more rigorous that that's the case when we do this multiple times.
So claim-- shortest paths are still preserved. All right, well, that's, again, not so difficult to see. Let's consider a path from s to t. It passes through a bunch of vertices. I'm going to label these as v0 to vk so that I can kind of number them.
All right, this is v1 here. This is a directed path, v1, 2, 3, 4, all the way up to k. There are k edges in this graph. I claim to you that any path from v0 to vk, any shortest path from v0 to vk remains a shortest path after I reweight everything in this way.
So let's say this is path pi, and so it has weight w pi, which is really just the sum of all of the edge weights from vi minus 1 to vi, for i equals 1 to k. This is poor notation. This is the weight of the edge from the vi minus 1 to i. And we've got-- it indexes from 1-- that's the first edge-- to k, which is the last edge. So that's the weight of my path.
The weight of my transformed path-- I'm going to do it down here. It's a little iffy. The weight of my transformed path I'm going to say is the weight in this new weighted graph G prime. This weight of that same path-- it's the same path-- is just going to be the sum of all of the reweighted edges. So i equals 1 to k of my original weight of my edge, so from 0, i minus 1 to vi.
But what did I do? This edge is outgoing from vi minus 1. So it's outgoing, so I add that weight-- that potential, sorry. But that edge is also incoming into vi. So when I reweighted the thing, I got a subtraction of h, vi.
Now, what happens here in the sum, this term, if I just took the sum over this term, that's exactly my original pathway. So that's good. But you'll notice that this sum has k terms, and this sum has the subtraction of k other terms. But most of these terms are equal.
Along the path, all the incoming and outgoing edges cancel out. So we're left with only adding the potential at the starting vertex and subtracting the potential at the final vertex. So we've got, add h, v0 minus h, vk.
And why is that good? Well, that's good because every path from v0 to vk starts at v0 and ends at vk. That's just-- that's how it is. That's how we've defined paths going from v0 to vk. But every such path, we transform the weight of that path by adding a constant associated with the start and adding this value associated with the end.
And so every path going from v0 to vk changes by the same amount. And so if this path pi was shortest, it's still shortest in the reweighted graph because I've just changed all paths between those two vertices by the same amount. This is kind of like a telescoping argument here in that kind of proof.
Right. So we have, the weight changes. It could change, but it changes all paths between these two vertices by the same amount, which means that shortest paths are still shortest. Awesome.
OK, so the name of the game here is now, we have this really flexible tool. We have this tool where we can add or subtract weight from various edges. But we have to do so in a kind of localized, constrained way. We have to do the same thing around each vertex. But it seems like a powerful transformation technique that maybe we can get this thing that we want, which is a G prime, a reweighting of the graph where all the edge weights are positive or non-negative.
So does there exist an h such that the weights are all positive? What does that mean? w prime u, v, the weight in my new graph, in G prime, I want these modified weights, this modified weight of my graph, I want each of these to be non-negative. So does there exist such a thing? Huh.
Well, if I rearrange this equation a little bit, this side, I get something that looks like this. h of v needs to be less than or equal to h of u, plus the weight of some edge from u to v. What does that look like? That looks like almost exactly the definition of the triangle inequality.
Shortest path from some vertex here and its shortest path distance from the same vertex here, this is just a statement of the triangle inequality. So if we can set these h's to be the shortest path distance from some vertex and those shortest path distances are finite, and not minus infinity, then this thing will hold by triangle inequality. And in particular, if we were to reweight the edges based on those values of h, then we get new edge rates that are non-negative.
Awesome. OK. But there might not be any vertex from which we can access, which we can reach all vertices in the graph. In particular, my graph might not even be connected. If I want this property, I need all of these-- I don't gain any information if these things are infinite. It's exhaustively true. Infinity is-- I don't even know how to compare infinity and infinity plus a constant. I don't know.
So I need all of these things to be finite. So how can I make those things finite? So here's the next idea. Add new vertex s with 0-weight edge to every vertex, V in V. We take our original graph. We add a new super node or auxiliary vertex s, with a 0-weight edge to every vertex in my graph.
What does that look like? This is like-- there's my original graph, and now I have this vertex s. But it has directed edges into all of the vertices with 0-weight. That's our picture. And this new thing I'm going to call, maybe, my s graph now.
And the claim is, well now, if I run some shortest path algorithm, single source shortest path algorithm this time, from s to compute the shortest path distance to all of the vertices, the shortest distance to each of the vertices can't be positive, because there's a 0-weight edge. So a minimum weight path is going to be no bigger than 0. If it's finite, then there's a finite length shortest path. If it's minus infinity, then there's a negative rate cycle in my graph and I can stop.
So there are either two situations. If delta s,v equals minus infinity-- so I guess this is, run single source shortest paths from s. And really, because this graph could contain negative edge weights and could contain negative cycles, we can't really do better than running Bellman-Ford here from s to compute these paths. If there exists in this new graph this Gs, if there exists a vertex that has negative infinite weight in the reweighted graph-- sorry, in the original graph G-- G hasn't been reweighted yet. If there's a negative weight distance from s, then there was a negative weight cycle in the original graph.
Why is that? Well, if this was set to minus infinity, then there is some negative weight cycle in the graph. The worry is that that negative weight cycle was added to my graph by adding this vertex s. But what do I know about vertex s? It has no incoming edges. So no negative weight cycle could go through s. So any negative weight cycle was in the original graph, and so I can abort. Abort. Yay.
Otherwise, what do I do? Well, I know the shortest path distances here would satisfy the triangle inequality. So if I reweight with h of v equal to delta s of v, if we set our potentials in our reweighted graph to be the shortest path distance from our super node s, it satisfies the triangle inequality. And because there's no negative cycles, all of these values are finite. And then this reweighting will lead to a graph with strictly-- or not strictly-- strictly no negative weights or non-negative weights. OK, great.
So that's basically it. That's the idea behind Johnson's algorithm. It's really a reduction problem or a reduction algorithm. We reducing from solving kind of signed all-pairs shortest paths, graphs where their weights could be positive or negative, and we're reducing to creating a graph that has the same shortest paths properties, but only has non-negative edge weights. So we're reducing from a signed context to a non-negative weight context.
So Johnson's algorithm, what are the steps? Construct Gs from G, just as up here. I make a new vertex s. I put a 0 weight directed edge from s to every vertex. So that's the first step.
Second step-- compute E, s,v for all V in V, i.e-- or e.g-- I guess really it should be i.e. because I don't really have another option here-- but by Bellman-Ford. This is just a single run of Bellman-Ford here. Compute. And then there are two possibilities.
If there exists a delta s, v that's minus infinity, then abort. Else, make-- or reweight the graph according to this reweighting scheme, by reweighting each edge in my original graph to have weight-- our new weight, which is our old weight, plus our transformation. Now, our transformation is now going to set h, v to this delta s, v. So I'm going to add delta s, u, and subtract delta s, v. That's our reweighting scheme. I'm just identifying h, v with this shortest path distance here.
And after I reweighted that, I can just solve all-pairs shortest paths on G prime with Dijkstra. And then compute G shortest path distances from G prime shortest path distances. Compute these distances from the other using this algorithm up here-- can compute distances in G from distances in G prime in linear time-- or sorry, v times linear time, linear time for each s-- for each vertex in my graph.
OK, so that's the algorithm. It's basically, correctness is trivial. We already proved-- the whole part of this lecture, the interesting part of this lecture was proving that, if we had a transformation based on a potential function that changed outgoing edges in a symmetrically opposite way as incoming edges, then that preserves shortest paths. And then realizing that the triangle inequality enforces this condition that edge weights will be non-negative under this reweighting, so we find shortest path distances from some other arbitrary vertex, and set our potential functions to be those shortest path distance weights.
We do the reweighting, because that reweighting preserves shortest paths, which we already argued. Then we can do-- then this has positive edge weights, so Dijkstra applies. And then computing this takes a small amount of time.
OK, what is the running time of this algorithm? So this part, reconstructing this thing, this takes linear time. I'm just adding. I'm just making a new graph of the same size, except I added v edges and one vertex. Computing-- doing Bellman-Ford on this new modified graph, that's just-- I'm doing that once. That takes V times E time.
Doing this check, that just takes-- I'm looping over my vertices. That just takes V time. Otherwise, doing this reweighting, I change the weight of every edge. That takes order E time. And then solving G prime-- solving all-pairs shortest paths on the modified edge weight graph with Dijkstra takes V times Dijkstra.
That's-- I could use a little bit more board space here. That's V times V log, V plus E time, which is actually the running time that we're looking for. I wanted to reduce to not using more than this time. We used this amount of time. Let's make sure we still didn't use even more.
After that, we compute these paths, as proofed before, in V times V plus E, which is smaller than that. And so summing up all of these running times, this one dominates. And so Johnson's can solve signed weighted all-pairs shortest paths, signed all-pairs shortest paths, not in V times Bellman-Ford, like we had before up here, but faster, in nearly linear for sparse graphs, just without this log factor. So we got quite a big improvement.
So that's the nice thing about all-pairs shortest paths is that, really, we don't have to incur this big cost in the context of negative weights. Essentially, we just run Bellman-Ford once to see if there is a negative weight cycle in my graph. If it is, I save a lot of work by stopping early.
So that's Johnson's. That's the end of our graphs lectures. We'll be having a review and problem session about how to solve problems, graph problems using this material. But we've talked about a lot of different things so far. We've talked about graph reachability, connected components, detecting cycles, detecting topological sort orders of a DAG.
We've talked about finding negative weight cycles, single source shortest path algorithms, and now finally, today, all-pairs shortest path algorithms, with a new algorithm that's really not an entirely new algorithm. We didn't have to do any proof by induction here. Really, the heavy work that's happening is we're reducing to using either Dijkstra or Bellman-Ford to do the heavy lifting of finding single source shortest paths efficiently. So Johnson's is really just glue to transform a graph in a clever way, and then reducing to using some of the shortest paths algorithms faster.
So that's our unit on graphs. Our next lecture, we'll start talking about a general form of, not presenting you with an algorithm, but how to design your own algorithm in the context of dynamic programming. So see you next lecture.