def get_random_flow_network(N: int) -> DirectedGraph:
        """
        Losowa sieć przepływu
        N - liczba warstw sieci
        źródło - Node #1
        ujście - Node #len(graph)
        """
        assert N >= 2

        print(f"liczba warstw: {N}")

        # krok 1: tworzenie warstw
        node_count_in_layer = [1] + [random.randint(2, N) for _ in range(N)] + [1]

        node_layer = [None]
        layer_nodes = []

        total = 1
        for i, count in enumerate(node_count_in_layer):
            node_layer.extend([i] * count)
            layer_nodes.append(list(range(total, total + count)))
            total += count

        print(f"liczba wierzchołków w warstwie: {node_count_in_layer}")
        print(f"wierzchołki w warstwie: {layer_nodes}")

        # krok 2: losowanie krawędzi między warstwami
        g = DirectedGraph(size=sum(node_count_in_layer))

        for i in range(1, N + 1):
            for node in layer_nodes[i]:
                # losowa krawędź wchodząca
                g.connect(random.choice(layer_nodes[i - 1]), node)
                # losowa krawędź wychodząca
                g.connect(node, random.choice(layer_nodes[i + 1]))

        # krok 3: odajemy 2N losowych łuków

        edges_added = 0
        while edges_added < 2 * N:
            # brak krawędzi wychodzącej z ujścia
            n1 = random.randint(1, len(g) - 1)
            # brak krawędzi wchodzącej do źródła
            n2 = random.randint(2, len(g))
            if n1 == n2 or g.is_connected(n1, n2) or g.is_connected(n2, n1):
                continue
            g.connect(n1, n2)
            edges_added += 1
        # krok 4: przypisanie każdej krawędzi losowej przepustowości
        g.assign_random_weights()

        return g
    def test_connect(self):
        g = DirectedGraph(8)
        n1 = 1
        n2 = 2

        g.connect(n1, n2)
        assert g.is_connected(n1, n2)
        assert not g.is_connected(n2, n1)

        g.disconnect(n1, n2)
        assert not g.is_connected(n1, n2)

        g.connect(n1, n2)
        g.connect(n2, n1)
        assert g.is_connected(n1, n2)
        assert g.is_connected(n2, n1)
def ford_fulkerson(g: DirectedGraph,
                   verbose: bool = False) -> Dict[Tuple[int, int], int]:
    """Edmonds–Karp implementation"""
    # sieć rezydualna
    gf = copy.deepcopy(g)
    # źródło
    s: Node = 1
    # ujście
    t: Node = len(g)
    # przepływ krawędzi
    f = {(e.begin, e.end): 0 for e in g.edges}

    step = 0

    while True:
        p = get_trail_to_node(breadth_first_search(gf, s, t), t)
        if p == [t]:
            if verbose:
                print("nie istnieje kolejna ścieżka rozszerzająca")
            break
        if verbose:
            print(f"ścieżka rozszerzająca: {p}")
        p_edges = [gf.edge_to_node(p[i - 1], p[i]) for i in range(1, len(p))]
        cf_p = min(e.weight for e in p_edges)
        if verbose:
            print(f"przepustowość rezydualna ścieżki: {cf_p}")
        for edge in p_edges:
            u = edge.begin
            v = edge.end
            if g.is_connected(u, v):
                f[(u, v)] = f[(u, v)] + cf_p
            else:
                f[(v, u)] = f[(v, u)] - cf_p
                if verbose:
                    print(f"kasowanie przepływu, krawędź: ({u}, {v})")
        # update residual network weights
        gf.edges = set()
        for u, v, c in {(e.begin, e.end, e.weight) for e in g.edges}:
            w1 = c - f[(u, v)]
            w2 = f[(u, v)]
            if w1 != 0:
                gf.connect(u, v, w1)
            if w2 != 0:
                gf.connect(v, u, w2)
        step += 1
    return f