algraph-0.7.0.0: src/Data/Graph/AdjacencyList/PushRelabel/Pure.hs
{-|
Module : Data.Graph.AdjacencyList.PushRelabel.Pure
Description : Tide algorithm — a push-pull-relabel max-flow solver
Copyright : Thodoris Papakonstantinou, 2017-2026
License : LGPL-3
Maintainer : dev@tpapak.com
Stability : experimental
Portability : POSIX
= Tide — Push (Pull) Relabel
The Tide algorithm is a push-relabel variant for solving the
<https://en.wikipedia.org/wiki/Maximum_flow_problem maximum flow problem>
on directed graphs.
== Definitions
A network \( N = (G, s, t, C) \) consists of a directed graph \( G \),
source \( s \), sink \( t \), and capacities \( C : E \to \mathbb{R}^+ \).
The /residual graph/ \( R \) contains both forward edges (with residual
capacity \( c - f \)) and backward edges (with capacity \( f \)).
Each vertex carries:
* __Height__ \( h(v) \): determines whether flow can be pushed along an edge
(flow moves from higher to lower height).
* __Excess__ \( x(v) \): records the net surplus of flow at \( v \).
At termination all excesses are zero and the preflow is a valid max flow.
* __Level__ \( \ell(v) \): the BFS distance from source in the /original/
graph \( G \). Constant throughout the algorithm. Determines the
sweep order.
== Operations
The key difference from classical push-relabel is that the push operation
is split into two:
* __Push__ (on forward edges): increases flow towards the sink.
* __Pull__ (on reverse edges): decreases flow, effectively pulling excess
backwards towards the source.
== Algorithm
Each iteration (\"tide\") consists of three global sweeps:
1. __globalRelabel__: BFS from sink (and source) on the residual graph to
recompute vertex heights. Source-side vertices get
\( h = |V| + d_s(v) \); sink-side vertices get \( h = d_t(v) \).
2. __globalPull__: /right fold/ over overflowing vertices in descending
level order, pulling flow on reverse edges (from sink towards source).
3. __globalPush__: /left fold/ over overflowing vertices in ascending
level order, pushing flow on forward edges (from source towards sink).
The algorithm terminates when both the net flow and the set of overflowing
vertices are unchanged between consecutive tides.
=== Skip-globalRelabel optimization
When no edge crosses a saturation boundary during push\/pull (the
'topologyChanged' flag is 'False'), the residual graph topology is
unchanged and globalRelabel is skipped. This saves 1.25--1.61x in
practice.
== Complexity
* Per-tide cost: \( O((V+E) \log V) \) with IntMap data structures.
* Number of tides: \( O(V^2) \) worst case (requires exponential capacity
ratios); \( O(V) \) in practice on non-pathological graphs.
* Total: \( O(V^2 (V+E) \log V) \) worst case;
\( O(V (V+E) \log V) \) practical.
See also the Rust implementation @tide-maxflow@ which achieves \( O(VE) \)
practical complexity using O(1) array-based data structures.
-}
{-# LANGUAGE BangPatterns #-}
module Data.Graph.AdjacencyList.PushRelabel.Pure
( -- * Main entry point
pushRelabel
-- * Algorithm internals (exported for testing)
, tide
, globalPush
, globalPull
, globalRelabel
) where
import Data.List
import Data.Maybe
import qualified Data.Map.Lazy as M
import qualified Data.IntMap.Lazy as IM
import qualified Data.IntSet as Set
import Control.Monad
import Data.Graph.AdjacencyList
import Data.Graph.AdjacencyList.Network
import Data.Graph.AdjacencyList.PushRelabel.Internal
import qualified Data.Graph.AdjacencyList.BFS as BFS
-- | Solve the maximum flow problem on a 'Network' using the Tide algorithm.
--
-- Returns @Right rg@ on success, where @rg@ is the 'ResidualGraph' at
-- termination. The maximum flow value is @netFlow rg@ and per-edge flows
-- are available via @edgeFlow rg e@ or via @flow (network rg)@.
--
-- Returns @Left msg@ if an internal invariant is violated (should not happen
-- on valid inputs).
--
-- ==== Example
--
-- @
-- let g = graphFromEdges [Edge 0 1, Edge 0 2, Edge 1 3, Edge 2 3]
-- caps = M.fromList [(Edge 0 1, 10), (Edge 0 2, 10), (Edge 1 3, 10), (Edge 2 3, 10)]
-- net = Network g 0 3 caps (M.fromList [(e, 0) | e <- edges g])
-- case pushRelabel net of
-- Right rg -> print (netFlow rg) -- 20
-- Left err -> putStrLn err
-- @
pushRelabel :: Network -> Either String ResidualGraph
pushRelabel net =
let initg = initializeResidualGraph net
res = tide initg 0
nvs = vertices $ graph $ network res
s = source net
t = sink net
insouts = filter (\v -> v /= s && v /= t && inflow res v < outflow res v) nvs
xsflows = filter (\v -> v /= s && v /= t && inflow res v - outflow res v /= excess res v) nvs
ofvs = IM.foldr (\ovs ac -> Set.union ac ovs) Set.empty $ overflowing res
notofvs = filter (\ ov ->
let (ResidualVertex v l h x) = fromJust (IM.lookup ov (netVertices res))
ml = (IM.lookup l (overflowing res))
in case ml of
Nothing -> True
Just os -> not $ Set.member ov os
) $ Set.toList $ getOverflowing $ netVertices res
errovfs = Set.filter (\v -> excess res v == 0) ofvs
in if null insouts && null xsflows && Set.null errovfs && null notofvs
then Right res
else
if not $ null insouts
then Left $ "Error Inflow < Outflow " ++ show insouts
else
if not $ null xsflows
then Left $ "Error vertex excess " ++ show xsflows
else
if not $ Set.null errovfs
then Left $ "Error not really overflowing " ++ show errovfs
else Left $ "Error not in overflowing " ++ show notofvs
++ " overflowings are " ++ show (overflowing res)
++ " nevertices are " ++ show (netVertices res)
-- | Core recursive loop of the Tide algorithm.
--
-- Each call performs one tide: globalRelabel (unless skipped), then
-- globalPull, then globalPush. Recurses until convergence (net flow
-- and overflowing set unchanged).
--
-- The @steps@ parameter counts completed iterations.
tide :: ResidualGraph -> Int -> ResidualGraph
tide rg steps =
let g = rg `seq` (graph $ network rg)
s = source $ network rg
t = sink $ network rg
es = edges g
vs = vertices g
olf = netFlow rg
-- Only run globalRelabel if the residual topology changed
relabeled = if topologyChanged rg
then globalRelabel rg
else rg
-- Reset flag before push/pull so we detect new changes
rg0 = relabeled { topologyChanged = False }
rg' = globalPush $ globalPull rg0 -- then global push and then global pull
nfl = netFlow rg'
steps' = steps + 1
oovfls = overflowing rg
novfls = overflowing rg'
in if nfl == olf -- if new flow == old flow
then
if oovfls == novfls -- and the overflowing nodes didn't change
then rg' { network = networkFromResidual rg' -- algorithm ends
, steps = steps'}
else tide rg' steps'
else tide rg' steps'
-- | Global push: sweep overflowing vertices from source to sink.
--
-- Iterates over overflowing vertices in /ascending level order/ (left fold
-- on the 'Overflowing' IntMap), pushing flow on all eligible /forward/
-- edges from each vertex.
--
-- This moves excess flow from source-side vertices towards the sink.
globalPush :: ResidualGraph -> ResidualGraph
globalPush rg =
let ovfs = overflowing rg
in IM.foldl' (\ac lset ->
Set.foldl' (\ac' v -> pushNeighbors ac' v)
ac lset
) rg ovfs
-- | Global pull: sweep overflowing vertices from sink to source.
--
-- Iterates over overflowing vertices in /descending level order/ (right fold
-- on the 'Overflowing' IntMap), pulling flow on all eligible /reverse/
-- edges to each vertex.
--
-- This moves excess flow from sink-side vertices back towards the source.
globalPull :: ResidualGraph -> ResidualGraph
globalPull rg =
let ovfs = overflowing rg
in IM.foldr' (\lset ac ->
Set.foldl' (\ac' v -> pullNeighbors ac' v)
ac lset
) rg ovfs
-- | Push flow through all forward residual neighbors of a vertex.
pushNeighbors :: ResidualGraph -> Vertex -> ResidualGraph
pushNeighbors g v =
let neimap = netNeighborsMap g
(fwdMap, _) = fromJust $ IM.lookup v neimap
feds = map (\n -> fromTuple (v,n)) $ IM.keys fwdMap
in foldl' (\ac e ->
let mv = push ac e
in case mv of
Nothing -> ac
Just g'' -> g'') g feds
-- | Pull flow through all reverse residual neighbors of a vertex.
pullNeighbors :: ResidualGraph -> Vertex -> ResidualGraph
pullNeighbors g v =
let neimap = netNeighborsMap g
(_, revMap) = fromJust $ IM.lookup v neimap
reds = map (\n -> fromTuple (n,v)) $ IM.keys revMap
in foldl' (\ac e ->
let mv = pull ac e
in case mv of
Nothing -> ac
Just g'' -> g'') g reds
-- | Global relabel: recompute vertex heights via BFS on the residual graph.
--
-- Runs BFS from both source and sink on the residual graph to compute
-- distances. Sets vertex heights:
--
-- * Sink-side vertices: @height = distance_from_sink@
-- * Source-side vertices: @height = |V| + distance_from_source@
--
-- The height gap between source-side and sink-side vertices ensures
-- that flow can only move from source-side to sink-side (downhill).
globalRelabel :: ResidualGraph -> ResidualGraph
globalRelabel rg =
let g = graph $ network rg
sh = numVertices g
s = source $ network rg
t = sink $ network rg
(slvs, tlvs) = residualDistances rg
-- Vertices not reached by either BFS get height 2*|V| so their
-- excess drains back to the source via pull operations.
allVs = Set.fromList (vertices g)
reachedS = Set.fromList (IM.keys slvs)
reachedT = Set.fromList (IM.keys tlvs)
reached = Set.union reachedS reachedT
unreached = Set.difference allVs reached
deadHeight = 2 * sh
rg0 = Set.foldl' (\ac v -> updateHeight ac v deadHeight) rg unreached
rg' = IM.foldrWithKey
(\ v l ac ->
-- Heights for the source partition vertices is N + their distance to the source
let h = sh + l
in updateHeight ac v h
) rg0 slvs
in IM.foldrWithKey (\ v h ac
-- Heights for the sink partition vertices equals the distance from sink
-> updateHeight ac v h)
rg' tlvs