# 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. – asterix314 Oct 8 '13 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. – Yves Klett Oct 8 '13 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. – asterix314 Oct 10 '13 at 15:42

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? – m_goldberg Oct 8 '13 at 9:01
@m_goldberg good idea, feel free to edit it whenever you want. – Kuba Oct 8 '13 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]}] – asterix314 Oct 8 '13 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? – asterix314 Oct 8 '13 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 '13 at 9:23

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 :) – István Zachar Oct 11 '13 at 14:02

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

Strange, my reduce drops 1 and leaves 5->4->3->2 for the last case of yours. – Kuba Oct 8 '13 at 9:55
@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)! – István Zachar Oct 8 '13 at 10:12
Or rather pay attention to 1 an 0 which are interpreted by logical functions ;) Implies["a", 1] // Simplify – Kuba Oct 8 '13 at 10:23