Skip to content

Optimize max flow with Dinic's blocking flow algorithm - #40

Open
shakayami wants to merge 1 commit into
masterfrom
claude/mf-graph-flow-dinic-5b1hht
Open

Optimize max flow with Dinic's blocking flow algorithm#40
shakayami wants to merge 1 commit into
masterfrom
claude/mf-graph-flow-dinic-5b1hht

Conversation

@shakayami

Copy link
Copy Markdown
Owner

Summary

Refactor the dfs function in the max flow implementation to use Dinic's blocking flow algorithm, which processes an entire level graph in one phase rather than finding augmenting paths one at a time. This improves the time complexity from O(V²E) to O(E√V) for unit-capacity graphs.

Key Changes

maxflow.py:

  • Rewrote dfs to compute a complete blocking flow for the current level graph in a single call, rather than returning after finding one augmenting path
  • Changed from a stack-based iterative approach to a path-tracing algorithm that walks backwards from sink to source
  • Implemented flow pushing in one sweep after finding the bottleneck capacity, then resuming from the first saturated edge instead of restarting
  • Optimized edge iteration by advancing past dead and saturated edges, with level updates to prevent revisiting unreachable nodes
  • Updated the main loop to call bfs() once per phase (not once per augmenting path), with Iter array reset per phase

benchmarks/workloads.py & benchmarks/README.md:

  • Updated bipartitematching cost factor from 0.15 to 5.0, reflecting the improved algorithm performance
  • Updated documentation to remove outdated notes about the O(V·E·flow) complexity and the low cost factor being a CI budget workaround
  • Clarified that only range_affine_range_sum and exp/log_of_formal_power_series run at reduced scale due to CPython limitations

Implementation Details

The new algorithm maintains a level graph via BFS and uses DFS to find augmenting paths within that level. Key optimizations include:

  • Reusing saturated edges' positions to resume path-finding efficiently
  • Computing bottleneck capacity across the entire path before updating residual capacities
  • Preventing redundant work by marking unreachable nodes with level = n

https://claude.ai/code/session_019maXFnaUfCveCBZFxmRmo7

flow()'s inner dfs returned after a single augmenting path, so the outer
loop rebuilt the level graph for every unit of flow.  On unit-capacity
bipartite matching that is O(V*E*flow) instead of Dinic's O(E*sqrt(V)):
the Library Checker bipartitematching workload took 163s at 20% of the
judge's size.

Rewrite dfs to push a whole blocking flow per call, matching ACL's
recursive version: it keeps walking t -> s with the current-arc
optimisation, and after each augmentation resumes from the first arc it
saturated instead of restarting at t.  Nodes with no usable predecessor
are still marked level = n so no later path retries them.

bipartitematching, same instance and same answer (matching = 15660):

    20% of judge size   162.69s -> 0.32s
    100% of judge size    ---   -> 2.91s

Checked against a separate Edmonds-Karp implementation on 33,000 random
graphs (self-loops, parallel edges, zero capacities, flow_limit cutoffs,
split flow calls), verifying the flow value, per-edge conservation and
that min_cut still matches the flow value.

Full size now fits the benchmark's one-to-four-second budget, so
bipartitematching goes back to COST_FACTOR 5.0 (judge maximum at the
default scale) and the sizing notes that described the old behaviour are
updated.
@github-actions

Copy link
Copy Markdown

📊 Performance Benchmark Results

Workloads ran at ACL_BENCH_SCALE=0.2.

⏱️ Library Checker workloads (best of N, lower is better)

Benchmark Base PR Delta Noise
assignment 2.772s 2.788s +0.5% ≈ ±15.9%
bipartitematching 3.914s 3.750s -4.2% ≈ ±4.3%
convolution_mod 1.823s 1.810s -0.7% ≈ ±4.2%
exp_of_formal_power_series 2.691s 2.721s +1.1% ≈ ±1.7%
factorize 119.38ms 110.56ms -7.4% ≈ ±13.4%
inv_of_formal_power_series 3.070s 3.106s +1.2% ≈ ±3.4%
log_of_formal_power_series 2.663s 2.671s +0.3% ≈ ±1.9%
number_of_substrings 1.575s 1.531s -2.7% ≈ ±17.3%
point_add_range_sum 1.015s 1.003s -1.3% ≈ ±7.0%
point_set_range_composite 1.808s 1.756s -2.9% ≈ ±8.9%
primality_test 345.09ms 358.03ms +3.8% ≈ ±4.1%
range_affine_range_sum 2.255s 2.187s -3.0% ≈ ±6.2%
scc 1.333s 1.305s -2.1% ≈ ±2.0%
staticrmq 1.461s 1.475s +0.9% ≈ ±11.2%
suffixarray 1.167s 1.170s +0.2% ≈ ±29.1%
sum_of_floor_of_linear 813.56ms 790.40ms -2.8% ≈ ±2.8%
two_sat 1.375s 1.422s +3.3% ⚠️ ±3.2%
unionfind 97.78ms 101.34ms +3.6% ≈ ±8.6%
zalgorithm 162.38ms 165.37ms +1.8% ≈ ±2.2%
🧪 API coverage workloads (not judge-calibrated)
Benchmark Base PR Delta Noise
extra_acl_math_crt 827.14ms 835.98ms +1.1% ≈ ±1.6%
extra_dsu_groups 316.92ms 298.53ms -5.8% ✅ ±4.4%
extra_maxflow_edges 183.62ms 168.25ms -8.4% ≈ ±9.1%
extra_mincostflow_slope 1.751s 1.746s -0.3% ≈ ±4.0%
extra_prime_fact_divisors 1.384s 1.366s -1.4% ≈ ±5.1%
extra_segtree_binary_search 174.50ms 171.75ms -1.6% ≈ ±1.7%
📏 Workload sizes
Benchmark Library Checker max Ran at Note
assignment N = 500 50% O(N^3)-ish in CPython; ACL_BENCH_SCALE=1 is far past the judge's TL
bipartitematching L = R = 100,000, M = 200,000 100%
convolution_mod N = M = 524,288 20%
exp_of_formal_power_series N = 500,000 10%
factorize Q = 100, A <= 10^18 100%
inv_of_formal_power_series N = 500,000 20%
log_of_formal_power_series N = 500,000 10%
number_of_substrings N = 500,000 100%
point_add_range_sum N = Q = 500,000 60%
point_set_range_composite N = Q = 500,000 40% segtree with a Python-level op; measures op-dominated cost
primality_test Q = 100,000, N <= 10^18 100%
range_affine_range_sum N = Q = 500,000 10%
scc N = M = 500,000 100%
staticrmq N = Q = 500,000 60% segtree with a C-level op (min); measures tree overhead
suffixarray N = 500,000 100%
sum_of_floor_of_linear T = 100,000 100%
two_sat N = M = 500,000 60%
unionfind N = Q = 200,000 100%
zalgorithm N = 500,000 100%
extra_acl_math_crt Q = 100,000 100% inv_mod / inv_gcd / crt
extra_dsu_groups N = 200,000 100% size / leader / groups
extra_maxflow_edges N = 20,000, M = 60,000 100% get_edge / change_edge / edges / min_cut
extra_mincostflow_slope N = 2,000, M = 8,000 100% slope / get_edge / edges
extra_prime_fact_divisors Q = 10,000 20% divisors / totient / lcm
extra_segtree_binary_search N = Q = 200,000 20% max_right / min_left
💾 Peak memory (lower is better)
Benchmark Base PR Delta
assignment 32.8MiB 32.8MiB 0.0%
bipartitematching 4.3MiB 142.7MiB +3195.8% ⚠️
convolution_mod 30.2MiB 30.2MiB 0.0%
exp_of_formal_power_series 21.7MiB 21.7MiB 0.0%
extra_acl_math_crt 484.0B 484.0B 0.0%
extra_dsu_groups 30.5MiB 30.5MiB 0.0%
extra_maxflow_edges 29.4MiB 29.4MiB 0.0%
extra_mincostflow_slope 5.5MiB 5.5MiB 0.0%
extra_prime_fact_divisors 8.6KiB 8.6KiB 0.0%
extra_segtree_binary_search 2.3MiB 2.3MiB 0.0%
factorize 836.0B 836.0B 0.0%
inv_of_formal_power_series 21.4MiB 21.4MiB 0.0%
log_of_formal_power_series 24.6MiB 24.6MiB 0.0%
number_of_substrings 82.5MiB 82.5MiB 0.0%
point_add_range_sum 12.1MiB 12.1MiB 0.0%
point_set_range_composite 34.9MiB 34.9MiB 0.0%
primality_test 804.0B 804.0B 0.0%
range_affine_range_sum 14.0MiB 14.0MiB 0.0%
scc 139.5MiB 139.5MiB 0.0%
staticrmq 8.1MiB 8.1MiB 0.0%
suffixarray 82.5MiB 82.5MiB 0.0%
sum_of_floor_of_linear 516.0B 516.0B 0.0%
two_sat 169.8MiB 169.8MiB 0.0%
unionfind 1.7MiB 1.7MiB 0.0%
zalgorithm 3.8MiB 3.8MiB 0.0%

Only the workloads whose module the change touches are measured; a scheduled sweep covers the rest. Each workload mirrors the linked Library Checker problem: same query mix, same operators, same value ranges as that problem's max_random generator. Times are the best observation across interleaved passes; Noise is the spread between passes. ✅ improvement / ⚠️ regression are only shown when the delta exceeds both 3% and the measured noise; ≈ means the difference is not distinguishable from noise. API-coverage workloads exist to catch regressions in methods no judge problem exercises — do not quote their timings as speedups. "n/a" rows mean one side has no such benchmark.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants