# Remove redundant dependencies from a directed acyclic graph

Given a directed acyclic graph, I want to remove all the edges $v_i \rightarrow v_j$ if $v_j$ is reachable from $v_i$ by some other path. Given the rich set of graph algorithms in Mathematica, what is the best way to achive this?

The background of the problem is this. I have a list of jobs and their dependencies, e.g., $a \rightarrow b$ means job $b$ can start only when job $a$ finishes. A list $\{a \rightarrow b, b \rightarrow c, a \rightarrow c\}$ is redundant because $a \rightarrow c$ is implied. I want to reduce the dependencies to a bare minimum.

• Algorithms I've looked at: minimum spanning tree, edge/vertex cover, shortest paths. But they are not what I'm looking for. Oct 8, 2013 at 6:21
• Including working code and showing what you already tried and what your input/output should look like will improve your chances to get an answer and make this question more useful for future visitors. At the moment there is about no Mathematica reference, too. Oct 8, 2013 at 6:34
• The solutions people have contributed are all of the "roll your own" type. It's a pity the rich set of graph algorithms in Mathematica are left unutilized for this question while we start from scratch. Oct 10, 2013 at 15:42
• The action that does this is called Transitive Reduction, explained in detail at en.wikipedia.org/wiki/Transitive_reduction Dec 10, 2019 at 8:54

Let $A$ be the adjacency matrix of the graph to be reduced. $A$ is also the reachability matrix for 1 hop, and $A^2$ for 2 hops and so on, if we substitue logical and ($\land$) for multiplication and logical or ($\lor$) for addition in multiplying two matrices. $A^k$ ($k<n$) will eventually be all zeros because we cannot have a path of $n$ hops or more where $n$ is the number of vertices (assuming no cycles).

Let $S = A^2 \lor A^3 \lor \cdots \lor A^k$ be the reachability matrix of 2 or more hops. To reduce $A$, we need to remove $i \rightarrow j$ in $A$ if it is also in $S$. The reduced adjacency matrix is therefore $A \land \lnot S$.

To put the above into code, note that we can just use normal multiplication and addition, after all, if we only look at the sign. This has a huge performance boost because we will be using highly optimized matrix multiplications on machine integers. We'll use Unitize to keep the intermediate results within the range of machine intergers:

reduce[a_] := a (1 - FixedPoint[Unitize[a.(a + #)] &, a.a])

• Neatly compacted! This seems to be 2x faster than my code working directly on Graphs, you should consider accepting your own answer :) Oct 11, 2013 at 14:02

I do not have experience with graphs and built-in functions related to them, but maybe something based on fact that the following is a Tautology:

$(a\Rightarrow b)\land (b\Rightarrow c)\Rightarrow (a\Rightarrow c)$

 And[Implies[a, b], Implies[b, c]]~Implies~Implies[a, c] // Simplify

True


Edit I've added temporary replacement for 1 and 0 which can cause a problems since they are interpreted by Simplify as True and False. More there: Simplify assumes..

list = {DirectedEdge[a, b], DirectedEdge[b, c], DirectedEdge[a, c]};

reduce[list_] := Module[{a, b}, With[{impl = Implies @@@ list /. {1 -> a, 0 -> b}},
DirectedEdge @@@ MapIndexed[
If[TrueQ @ Simplify @ Implies[And @@ Drop[impl, #2], #1],
Unevaluated[Sequence[]], #1] &
, impl]
] /. {a -> 1, b -> 0}]

reduce[list]

{DirectedEdge[a, b], DirectedEdge[b, c]}


### Edit by m_goldberg

I think it is is worth looking at some graphs a little more complex than the one the OP mentioned, both before and after reduce is applied to them.

dag2 = DirectedEdge @@@ {{a, b}, {b, c}, {a, c}, {e, b}, {e, c}};
dag3 = DirectedEdge @@@ {{a, b}, {b, c}, {a, c}, {e, b}, {e, c}, {e, f}, {f, c}};
dag4 = DirectedEdge @@@ {{2, 1}, {3, 1}, {3, 2}, {4, 1}, {4, 2}, {4, 3}, {5, 1},
{5, 2}, {5, 3}, {5, 4}}; (*István's example*)

dags = {#, reduce[#]} & /@ {dag2, dag3, dag4}
gridData = Prepend[
Map[Graph[#, VertexLabels -> "Name", GraphLayout -> "SpringEmbedding"] &,
dags, {2}],
{"Before", "After"}];
Grid[gridData, Frame -> All]


• I think it does work well. I had to do some testing with a couple of more interesting data sets to convince myself of it. Since I had the test notebook at hand, I thought why not post the tests as an illustration of the method? Oct 8, 2013 at 9:01
• @m_goldberg good idea, feel free to edit it whenever you want.
– Kuba
Oct 8, 2013 at 9:08
• Thanks Kuba for the clever idea of reformulating the problem using the language of logic. Along the same lines I got the following: reduce2[list_] := With[{impl = And @@ (list /. DirectedEdge -> Implies)}, List @@ BooleanMinimize[impl, "CNF"] /. {! a_ || b_ :> DirectedEdge[a, b], b_ || ! a_ :> DirectedEdge[a, b]}] Oct 8, 2013 at 9:13
• I was actually looking for graph-specific algorithms. This detour to the logic method is very refreshing indeed. But there must be a graph algorithm to it, too? Oct 8, 2013 at 9:21
• @asterix314 I'm glad you find it useful. To make it even more compact you can write Implies @@@ list instead of list /. DirectedEdge->Implies. I used this because I'm not experienced with Graphs. So maybe better hold on with an accept to not discourage others in finding quicker way :)
– Kuba
Oct 8, 2013 at 9:23

I used this generator algorithm for DAGs (by Szabolcs):

{vertices, edges} = {7, 10};
elems = RandomSample@PadRight[ConstantArray[1, edges], vertices (vertices-1)/2];
adj = Take[FoldList[RotateLeft, elems, Range[0, vertices-2]], All,
vertices]~LowerTriangularize~-1;
EdgeList@g

{2 -> 1, 3 -> 1, 3 -> 2, 4 -> 1, 4 -> 2, 4 -> 3, 5 -> 1, 5 -> 2, 5 -> 3, 5 -> 4}


Removing redundant edges iteratively:

new = Graph[Flatten[If[GraphDistance[EdgeDelete[g, #], First@#,
Last@#] < Infinity, {}, #] & /@ EdgeList@g],
VertexLabels -> "Name", ImagePadding -> 10];
Row@{HighlightGraph[g, new, VertexLabels -> "Name", ImagePadding -> 10], new}


For some graphs, the remaining graph is simply the path graph of the topologically sorted vertices:

g = Graph[{2->1, 3->1, 3->2, 4->1, 4->2, 4->3, 5->1, 5->2, 5->3, 5->4}];


Note that this method removes unconnected singletons.

Here is a version that works directly on adjacency matrices. This should be faster than working on huge Graph objects directly.

The removableQ function recursively tests if the node from has an alternative route to to than the direct one, by collecting children nodes. The moment the function finds another edge terminating at to, exits from the loop, as it is unnecessary to check further.

removableQ[m_, {from_, to_}] := Module[{children},
children = Flatten@Position[m[[from]], 1];
If[MemberQ[children, to], Throw@to,
Do[removableQ[m, {i, to}], {i, children}]; None]
];


The wrapper reduce iterates through all edges in the matrix:

reduce[adj_] := Module[{edgeList = Position[adj, 1], rem},
rem = DeleteCases[{First@#,
Catch@removableQ[ReplacePart[adj, # -> 0], #]} & /@
edgeList, {_, None}];
];


Let's call reduce on a random DAG's adjecency matrix:

g = DirectedGraph[RandomGraph[{6, 10}], "Acyclic"];
EdgeList@g

{1 -> 3, 1 -> 4, 1 -> 5, 1 -> 6, 2 -> 3, 2 -> 4, 2 -> 5, 3 -> 5, 4 -> 6, 5 -> 6}

adj = Normal@AdjacencyMatrix@g


Note that this method does not remove unconnected singletons.

• Strange, my reduce drops 1 and leaves 5->4->3->2 for the last case of yours.
– Kuba
Oct 8, 2013 at 9:55
• @Kuba It should keep 1. I still cannot figure out whether there is always one exact reduction or it could depend on the order edges are removed. Oct 8, 2013 at 10:02
• @Kuba It removes the 1 because Simplify@Implies[(3 => 1) && (3 => 2), 2 => 1] returns True, though it shouldn't for a graph (if you have edges 3->1 and 3->2 you don't necessarily have 2->1)! Oct 8, 2013 at 10:12
• Or rather pay attention to 1 an 0 which are interpreted by logical functions ;) Implies["a", 1] // Simplify
– Kuba
Oct 8, 2013 at 10:23
• @Kuba Oh, sure, that buglike feature got me once. Quite annoying! Oct 8, 2013 at 10:32

TransitiveReductionGraph (version 10+)

Using the examples from @Kuba's answer:

dag1 = {DirectedEdge[a, b], DirectedEdge[b, c], DirectedEdge[a, c]};
dag2 = DirectedEdge @@@ {{a, b}, {b, c}, {a, c}, {e, b}, {e, c}};
dag3 = DirectedEdge @@@ {{a, b}, {b, c}, {a, c}, {e, b}, {e, c}, {e,  f}, {f, c}};
dag4 = DirectedEdge @@@ {{2, 1}, {3, 1}, {3, 2}, {4, 1}, {4, 2}, {4, 3},
{5, 1}, {5, 2}, {5, 3}, {5, 4}};
options = {VertexLabels -> Placed["Name", Center], VertexSize -> Large, ImageSize -> Small}

Grid[Prepend[Through[{Graph[#, options]&, TransitiveReductionGraph[#, options]&}@#] & /@
{dag1, dag2, dag3, dag4},
{Style["g", 16, Bold], Style["TransitiveReductionGraph[g]", 16, Bold] }],
Dividers -> All, ItemSize -> {{Scaled[.3], Scaled[.3]}, {Scaled[.1], 5, 5, 5, 5}}]


Note: Although it works as expected in the cases considered here, as noted by Szabolcs, TransitiveReductionGraph had unresolved issues before version 12.1.