22
$\begingroup$

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.

$\endgroup$
4
  • $\begingroup$ Algorithms I've looked at: minimum spanning tree, edge/vertex cover, shortest paths. But they are not what I'm looking for. $\endgroup$
    – asterix314
    Commented Oct 8, 2013 at 6:21
  • 1
    $\begingroup$ 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. $\endgroup$
    – Yves Klett
    Commented Oct 8, 2013 at 6:34
  • 1
    $\begingroup$ 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. $\endgroup$
    – asterix314
    Commented Oct 10, 2013 at 15:42
  • 1
    $\begingroup$ The action that does this is called Transitive Reduction, explained in detail at en.wikipedia.org/wiki/Transitive_reduction $\endgroup$ Commented Dec 10, 2019 at 8:54

4 Answers 4

11
$\begingroup$

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])
$\endgroup$
1
  • $\begingroup$ Neatly compacted! This seems to be 2x faster than my code working directly on Graphs, you should consider accepting your own answer :) $\endgroup$ Commented Oct 11, 2013 at 14:02
7
$\begingroup$

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]

enter image description here

$\endgroup$
5
  • $\begingroup$ 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? $\endgroup$
    – m_goldberg
    Commented Oct 8, 2013 at 9:01
  • $\begingroup$ @m_goldberg good idea, feel free to edit it whenever you want. $\endgroup$
    – Kuba
    Commented Oct 8, 2013 at 9:08
  • 1
    $\begingroup$ 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]}] $\endgroup$
    – asterix314
    Commented Oct 8, 2013 at 9:13
  • $\begingroup$ 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? $\endgroup$
    – asterix314
    Commented Oct 8, 2013 at 9:21
  • 1
    $\begingroup$ @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 :) $\endgroup$
    – Kuba
    Commented Oct 8, 2013 at 9:23
6
$\begingroup$

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;
g = AdjacencyGraph[adj, DirectedEdges -> True];
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}

Mathematica graphics

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}];

Mathematica graphics

Note that this method removes unconnected singletons.


Adjacency matrix version

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}];
   ReplacePart[adj, Thread[rem -> 0]]
   ];

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
new = reduce@adj;
Row@{g, AdjacencyGraph@new}

Mathematica graphics

Note that this method does not remove unconnected singletons.

$\endgroup$
14
  • $\begingroup$ Strange, my reduce drops 1 and leaves 5->4->3->2 for the last case of yours. $\endgroup$
    – Kuba
    Commented Oct 8, 2013 at 9:55
  • $\begingroup$ @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. $\endgroup$ Commented Oct 8, 2013 at 10:02
  • $\begingroup$ @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)! $\endgroup$ Commented Oct 8, 2013 at 10:12
  • $\begingroup$ Or rather pay attention to 1 an 0 which are interpreted by logical functions ;) Implies["a", 1] // Simplify $\endgroup$
    – Kuba
    Commented Oct 8, 2013 at 10:23
  • 1
    $\begingroup$ @Kuba Oh, sure, that buglike feature got me once. Quite annoying! $\endgroup$ Commented Oct 8, 2013 at 10:32
3
$\begingroup$

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}}]

enter image description here

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

$\endgroup$
4
  • 1
    $\begingroup$ Note that TransitiveReductionGraph is buggy and often returns wrong results. $\endgroup$
    – Szabolcs
    Commented Feb 19, 2018 at 19:45
  • 1
    $\begingroup$ @Szabolcs, thank you; added a note re the bug. $\endgroup$
    – kglr
    Commented Feb 21, 2018 at 23:29
  • $\begingroup$ @kglr: Is the bug problem with TransitiveReductionGrap solved in version 13.1 version? $\endgroup$ Commented Nov 2, 2023 at 14:18
  • $\begingroup$ @kglr: In this link https://mathematica.stackexchange.com/q/83852/60365, it says that the problem is solved, but I am not sure if it is really solved! $\endgroup$ Commented Nov 2, 2023 at 14:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.