Performs a transitive reduction on the DAG. The transitive
        reduction of a graph is a graph with as few edges as possible with the
        same reachability as the original graph.

        See https://en.wikipedia.org/wiki/Transitive_reduction

def transitive_reduction(self):
        """ Performs a transitive reduction on the DAG. The transitive
        reduction of a graph is a graph with as few edges as possible with the
        same reachability as the original graph.

        See https://en.wikipedia.org/wiki/Transitive_reduction
        """
        combinations = []
        for node, edges in self.graph.items():
            combinations += [[node, edge] for edge in edges]

        while True:
            new_combinations = []
            for comb1 in combinations:
                for comb2 in combinations:
                    if not comb1[-1] == comb2[0]:
                        continue
                    new_entry = comb1 + comb2[1:]
                    if new_entry not in combinations:
                        new_combinations.append(new_entry)
            if not new_combinations:
                break
            combinations += new_combinations

        constructed = {(c[0], c[-1]) for c in combinations if len(c) != 2}
        for node, edges in self.graph.items():
            bad_nodes = {e for n, e in constructed if node == n}
            self.graph[node] = edges - bad_nodes