"""
Push-relabel (Goldberg-Tarjan) algorithm for the maximum-flow problem.

The push-relabel method takes a very different approach from the augmenting-path
algorithms in this directory (``ford_fulkerson.py`` builds up a valid flow one
path at a time).  Instead it works with a *preflow*, in which a vertex may
temporarily receive more flow than it sends out.  Each active vertex either
*pushes* its excess towards a neighbour that is one level lower, or is *relabeled*
to a higher level so that a push becomes possible.  When no vertex other than the
source and sink has excess, the preflow has become a maximum flow.

Using the highest-label selection rule (always discharge an active vertex whose
label is largest) this implementation runs in O(V^2 * sqrt(E)) time, which beats
the augmenting-path methods on dense graphs.

Reference: https://en.wikipedia.org/wiki/Push%E2%80%93relabel_maximum_flow_algorithm
"""

from __future__ import annotations


class PushRelabel:
    """
    Maximum flow in a directed graph with non-negative integer capacities.

    Add edges with :meth:`add_edge`, then call :meth:`max_flow`.

    >>> g = PushRelabel(6)
    >>> capacities = {
    ...     (0, 1): 16, (0, 2): 13, (1, 2): 10, (1, 3): 12,
    ...     (2, 1): 4, (2, 4): 14, (3, 2): 9, (3, 5): 20,
    ...     (4, 3): 7, (4, 5): 4,
    ... }
    >>> for (u, v), cap in capacities.items():
    ...     g.add_edge(u, v, cap)
    >>> g.max_flow(0, 5)
    23

    It agrees with the classic four-vertex example:

    >>> h = PushRelabel(4)
    >>> for (u, v), cap in {(0, 1): 3, (0, 2): 2, (1, 2): 5,
    ...                     (1, 3): 2, (2, 3): 3}.items():
    ...     h.add_edge(u, v, cap)
    >>> h.max_flow(0, 3)
    5

    Parallel edges add up, and a disconnected sink gives zero flow:

    >>> p = PushRelabel(2)
    >>> p.add_edge(0, 1, 3)
    >>> p.add_edge(0, 1, 5)
    >>> p.max_flow(0, 1)
    8
    >>> PushRelabel(3).max_flow(0, 2)
    0
    """

    def __init__(self, vertices: int) -> None:
        if vertices <= 0:
            raise ValueError("number of vertices must be positive")
        self.size = vertices
        self.graph: list[list[int]] = [[] for _ in range(vertices)]
        # Each edge is stored as [destination, residual_capacity].
        self.edges: list[list[int]] = []

    def add_edge(self, source: int, destination: int, capacity: int) -> None:
        """
        Add a directed edge ``source -> destination`` with the given capacity.

        >>> g = PushRelabel(2)
        >>> g.add_edge(0, 1, -1)
        Traceback (most recent call last):
            ...
        ValueError: capacity must be non-negative
        >>> g.add_edge(2, 0, 1)
        Traceback (most recent call last):
            ...
        ValueError: vertex out of range
        """
        if capacity < 0:
            raise ValueError("capacity must be non-negative")
        if not (0 <= source < self.size and 0 <= destination < self.size):
            raise ValueError("vertex out of range")
        self.graph[source].append(len(self.edges))
        self.edges.append([destination, capacity])
        self.graph[destination].append(len(self.edges))
        self.edges.append([source, 0])  # reverse edge starts saturated

    def max_flow(self, source: int, sink: int) -> int:
        """
        Return the maximum flow from ``source`` to ``sink``.

        >>> PushRelabel(2).max_flow(0, 0)
        Traceback (most recent call last):
            ...
        ValueError: source and sink must be different
        """
        if not (0 <= source < self.size and 0 <= sink < self.size):
            raise ValueError("vertex out of range")
        if source == sink:
            raise ValueError("source and sink must be different")

        height = [0] * self.size
        excess = [0] * self.size
        height[source] = self.size

        # Saturate every edge leaving the source to create the initial preflow.
        for edge_index in self.graph[source]:
            destination, residual = self.edges[edge_index]
            if residual > 0:
                self.edges[edge_index][1] -= residual
                self.edges[edge_index ^ 1][1] += residual
                excess[destination] += residual
                excess[source] -= residual

        active = [
            v for v in range(self.size) if v not in (source, sink) and excess[v] > 0
        ]

        while active:
            u = max(active, key=lambda v: height[v])
            if not self._discharge(u, height):
                # Relabel: lift u just above its lowest usable neighbour.
                min_height = min(
                    height[self.edges[i][0]]
                    for i in self.graph[u]
                    if self.edges[i][1] > 0
                )
                height[u] = min_height + 1
            self._apply_pushes(u, height, excess)
            active = [
                v for v in range(self.size) if v not in (source, sink) and excess[v] > 0
            ]

        return excess[sink]

    def _discharge(self, u: int, height: list[int]) -> bool:
        """Return ``True`` if ``u`` has at least one admissible outgoing edge."""
        return any(
            self.edges[i][1] > 0 and height[self.edges[i][0]] == height[u] - 1
            for i in self.graph[u]
        )

    def _apply_pushes(self, u: int, height: list[int], excess: list[int]) -> None:
        """Push as much excess as possible from ``u`` along admissible edges."""
        for edge_index in self.graph[u]:
            if excess[u] == 0:
                break
            destination, residual = self.edges[edge_index]
            if residual > 0 and height[u] == height[destination] + 1:
                delta = min(excess[u], residual)
                self.edges[edge_index][1] -= delta
                self.edges[edge_index ^ 1][1] += delta
                excess[u] -= delta
                excess[destination] += delta


if __name__ == "__main__":
    from doctest import testmod

    testmod()
