Ask Your Question

Revision history [back]

Consider the following method that uses integer linear programming to enumerate the minimal directed cuts.

def directed_edge_cuts_MIP(D):
    r"""
    Return an iterator over the directed cuts of ``D``.

    This method uses mixed integer linear programming to enumerate the
    minimal directed cuts of ``D``.
    """
    int_to_vertex = list(D)
    vertex_to_int = {u: i for i, u in enumerate(int_to_vertex)}
    H = D.relabel(perm=vertex_to_int, inplace=False)

    def good_edges(edges):
        return [(int_to_vertex[u], int_to_vertex[v]) for u, v in edges]

    def hedge(u, v):
        return (u, v) if u < v else (v, u) 

    from sage.numerical.mip import MixedIntegerLinearProgram
    p = MixedIntegerLinearProgram(constraint_generation=True, maximization=False)
    x = p.new_variable(binary=True)
    y = p.new_variable(binary=True)
    z = p.new_variable(binary=True)

    # At least one vertex must be 0 and at least one 1
    p.add_constraint(p.sum(x[u] for u in H), min=1, max=H.order() - 1)

    # underlying edge (u, v) is in the cut if u != v
    for u, v in H.edge_iterator(labels=False):
        p.add_constraint(x[v] - x[u] <= y[hedge(u, v)])
        p.add_constraint(x[u] - x[v] <= y[hedge(u, v)])

    # an edge of H is in the cut iff x[u] == 0 and x[v] == 1
    for u, v in H.edge_iterator(labels=False):
        p.add_constraint(x[v] - x[u] <= z[u, v])
        p.add_constraint(z[u, v] <= x[v])
        p.add_constraint(z[u, v] <= 1 - x[u])
        p.add_constraint(y[hedge(u,v)] <= z[u, v])

    p.set_objective(p.sum(z[u, v] for u, v in  H.edge_iterator(labels=False)))

    # get trivial cuts when a vertex has no incoming or outgoing edge
    for u in H:
        cut = []
        if not H.in_degree(u):
            cut = H.outgoing_edges(u, labels=False)
        elif not H.out_degree(u):
            cut = H.incoming_edges(u, labels=False)
        if cut:
            # yield the cut
            yield good_edges(cut)
            # and add a constraint to prevent finding it again
            p.add_constraint(p.sum(z[u, v] for u, v in cut) <= len(cut) - 1)

    # Now search for non trivial cuts
    while True:
        try:
            p.solve()
        except:
            # no more solution
            return

        # Extract the cut
        zz = p.get_values(z, convert=bool, tolerance=1e-3)
        cut = [e for e, b in zz.items() if b]
        # yield it
        yield good_edges(cut)
        # and add a constraint to prevent finding it again
        p.add_constraint(p.sum(z[u, v] for u, v in cut) <= len(cut) - 1)

    return

We can use a frontend method that exploits the digraph of the strongly connected components. Recall that we cannot find a directed cut in a strongly connected graph.

def directed_edge_cuts(D):
    r"""
    Return an iterator over the minimal directed edge cuts of ``D``.
    """
    H = D.strongly_connected_components_digraph()

    def good_cut(cut):
        """
        Turn a cut in H to a cut in D
        """
        return [e for u, v in cut for e in D.edge_boundary(u, v, labels=False)]

    # We compute the 2 edge-connected components of the underlying graph
    # Each block of order 2 corresponds to a minimal directed cut in D
    for block in H.blocks_and_cut_vertices()[0]:
        if len(block) == 2:
            u, v = block
            if not H.has_edge(u, v):
                u, v = v, u
            # yield the cut and remove it from H
            yield good_cut([(u, v)])
            H.delete_edge(u, v)

    # We now search for cuts in the connected components
    for g in H.connected_components_subgraphs():
        if g.order() > 1:
            for cut in directed_edge_cuts_MIP(g):
                yield good_cut(cut)

To get the dijoins, we can also use integer linear programming.

def dijoins(D):
    r"""
    Return an iterator over the minimal cardinality dijoins in ``D``.

    A dijoin is a set of edges that intersects all minimal directed cuts.
    """
    from sage.numerical.mip import MixedIntegerLinearProgram
    p = MixedIntegerLinearProgram(constraint_generation=True, maximization=False)
    x = p.new_variable(binary=True)

    # A dijoin contains at least one edge per cut
    for cut in directed_edge_cuts(D):
        p.add_constraint(p.sum(x[e] for e in cut), min=1)

    p.set_objective(p.sum(x[e] for e in D.edge_iterator(labels=False)))

    size = 0

    while True:
        try:
            p.solve()
        except:
            return

        # Extract the dijoin
        xx = p.get_values(x, convert=bool, tolerance=1e-3)
        dijoin = [e for e, b in xx.items() if b]
        if not size:
            size = len(dijoin)
        elif size < len(dijoin):
            # no more minimal cardinality dijoin
            return

        # yield it
        yield dijoin
        # and add a constraint to prevent finding it again
        p.add_constraint(p.sum(x[e] for e in dijoin) <= len(dijoin) - 1)

    return

all these methods are iterators.

sage: D = DiGraph([
....:     (1,4), (1,5),
....:     (2,4), (2,5), (2,6),
....:     (3,4), (3,5), (3,6)
....: ])
sage: print("Minimal directed cuts:")
....: for i, cut in enumerate(directed_edge_cuts(D), 1):
....:     print(f"Directed Cut {i}: {cut}")
Minimal directed cuts:
Directed Cut 1: [(3, 5), (2, 5), (1, 5)]
Directed Cut 2: [(3, 5), (3, 6), (3, 4)]
Directed Cut 3: [(3, 6), (2, 6)]
Directed Cut 4: [(2, 5), (2, 6), (2, 4)]
Directed Cut 5: [(3, 4), (2, 4), (1, 4)]
Directed Cut 6: [(1, 5), (1, 4)]
Directed Cut 7: [(3, 5), (3, 4), (2, 5), (2, 4)]
sage: print("Minimal dijoins:")
....: for i, dj in enumerate(dijoins(D), 1):
....:     print(f"Dijoin {i}: {dj}")
Minimal dijoins:
Dijoin 1: [(3, 5), (2, 6), (1, 4)]
Dijoin 2: [(1, 5), (3, 4), (2, 6)]
Dijoin 3: [(2, 5), (3, 6), (1, 4)]
Dijoin 4: [(1, 5), (3, 6), (2, 4)]

This is consistent with your solutions. I let you check the next example.

sage: D = DiGraph([
....:     (1,13), (1,14),
....:     (2,13), (2,14), (2,15),
....:     (3,10), (3,11), (3,12), (3,13), (3,14), (3,15), (3,17), (3,19), (3,20),
....:     (4,10), (4,11), (4,12), (4,13), (4,14), (4,15), (4,17), (4,19), (4,20),
....:     (5,19), (5,20),
....:     (6,13), (6,14), (6,16), (6,18),
....:     (7,10), (7,11), (7,12),
....:     (8,13), (8,14), (8,16), (8,18),
....: ])
sage: print("Minimal directed cuts:")
....: for i, cut in enumerate(directed_edge_cuts(D), 1):
....:     print(f"Directed Cut {i}: {cut}")
....: 
Minimal directed cuts:
Directed Cut 1: [(6, 13), (2, 13), (1, 13), (8, 13), (4, 13), (3, 13)]
Directed Cut 2: [(6, 13), (6, 16), (6, 18), (6, 14)]
Directed Cut 3: [(6, 16), (8, 16)]
Directed Cut 4: [(8, 13), (8, 16), (8, 18), (8, 14)]
Directed Cut 5: [(6, 18), (8, 18)]
Directed Cut 6: [(6, 14), (2, 14), (1, 14), (8, 14), (4, 14), (3, 14)]
Directed Cut 7: [(4, 13), (4, 15), (4, 14), (4, 10), (4, 11), (4, 12), (4, 17), (4, 20), (4, 19)]
Directed Cut 8: [(4, 10), (3, 10), (7, 10)]
Directed Cut 9: [(3, 13), (3, 15), (3, 14), (3, 10), (3, 11), (3, 12), (3, 17), (3, 20), (3, 19)]
Directed Cut 10: [(4, 11), (3, 11), (7, 11)]
Directed Cut 11: [(7, 10), (7, 11), (7, 12)]
Directed Cut 12: [(4, 12), (3, 12), (7, 12)]
Directed Cut 13: [(4, 17), (3, 17)]
Directed Cut 14: [(4, 20), (3, 20), (5, 20)]
Directed Cut 15: [(5, 20), (5, 19)]
Directed Cut 16: [(4, 19), (3, 19), (5, 19)]
Directed Cut 17: [(2, 15), (4, 15), (3, 15)]
Directed Cut 18: [(2, 13), (2, 15), (2, 14)]
Directed Cut 19: [(1, 13), (1, 14)]
Directed Cut 20: [(4, 20), (4, 19), (3, 20), (3, 19)]
Directed Cut 21: [(6, 13), (6, 14), (8, 13), (8, 14)]
Directed Cut 22: [(4, 13), (4, 15), (4, 14), (3, 13), (3, 15), (3, 14)]
Directed Cut 23: [(4, 13), (4, 14), (3, 13), (3, 14), (2, 13), (2, 14)]
Directed Cut 24: [(4, 10), (4, 11), (4, 12), (3, 10), (3, 11), (3, 12)]
sage: %time A = list(directed_edge_cuts(D))
CPU times: user 48.2 ms, sys: 1.88 ms, total: 50.1 ms
Wall time: 48.5 ms
sage:
sage: print("Minimal dijoins:")
....: for i, dj in enumerate(dijoins(D), 1):
....:     print(f"Dijoin {i}: {dj}")
....: 
Minimal dijoins:
Dijoin 1: [(1, 13), (4, 13), (6, 18), (6, 14), (8, 16), (4, 12), (4, 17), (4, 20), (3, 10), (7, 11), (5, 19), (2, 15)]
Dijoin 2: [(1, 13), (4, 13), (6, 18), (6, 14), (8, 16), (4, 12), (4, 20), (3, 10), (3, 17), (7, 11), (5, 19), (2, 15)]
Dijoin 3: [(6, 13), (4, 13), (6, 18), (8, 16), (1, 14), (4, 12), (4, 17), (4, 20), (3, 10), (7, 11), (5, 19), (2, 15)]
Dijoin 4: [(6, 13), (4, 13), (6, 16), (8, 18), (1, 14), (4, 12), (4, 17), (4, 20), (3, 10), (7, 11), (5, 19), (2, 15)]
Dijoin 5: [(6, 13), (4, 13), (6, 16), (8, 18), (1, 14), (4, 12), (4, 17), (4, 20), (7, 10), (3, 11), (5, 19), (2, 15)]
Dijoin 6: [(6, 13), (4, 13), (6, 18), (8, 16), (1, 14), (4, 12), (4, 20), (3, 10), (3, 17), (7, 11), (5, 19), (2, 15)]
Dijoin 7: [(6, 13), (4, 13), (6, 16), (8, 18), (1, 14), (4, 12), (4, 20), (3, 10), (3, 17), (7, 11), (5, 19), (2, 15)]
Dijoin 8: [(6, 13), (4, 13), (6, 16), (8, 18), (1, 14), (4, 10), (4, 12), (4, 20), (3, 17), (7, 11), (5, 19), (2, 15)]
Dijoin 9: [(6, 13), (4, 13), (6, 16), (8, 18), (1, 14), (4, 10), (4, 11), (4, 20), (3, 17), (7, 12), (5, 19), (2, 15)]
Dijoin 10: [(2, 13), (1, 13), (6, 18), (6, 14), (8, 16), (4, 15), (4, 12), (4, 17), (4, 20), (3, 10), (7, 11), (5, 19)]
.....

I don't know how many dijoins we get for this graph. It's a large number...