Skip to content

bipartite_matching.hpp

SECTIONGraph INCLUDEnoya/graph/bipartite_matching.hpp

稀疏二分图最大匹配与稠密位集匹配;稠密版还可恢复最小点覆盖。支持重边、空图和长增广路;稀疏版不再打乱输入,稠密版支持动态位集和增边后增广。

Complexity: Time: \(O(V + E \sqrt{V})\) sparse; \(O(V + W + N_{1} \cdot (N_{1} \cdot W + N_{2}))\) dense, with \(W\) words per row. Space: \(O(V + E)\) sparse; \(O(V + W)\) dense auxiliary plus \(O(N_{1} \cdot W)\) adjacency.

Source: maspy (dense matcher)

AC 记录:bipartitematching。

跳到代码 · GitHub ↗

Implementation

当前头文件,省略 include guard;依赖见 #include。

/// @complexity Time: O(V + E sqrt(V)) sparse;
/// O(V + W + N1 * (N1 * W + N2)) dense, with W words per row.
/// Space: O(V + E) sparse; O(V + W) dense auxiliary plus O(N1 * W) adjacency.

#include <algorithm>
#include <bitset>
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_matching_internal {

// GNU bitset and noya::dynamic_bitset have different find-next conventions.
// Other bitsets (including libc++ std::bitset) use a portable bitwise scan.
// For that fallback, replace W by the row's bit capacity in the time bound.
// dynamic_bitset is supplied by noya/ds/dynamic_bitset.hpp when needed.
template <class BS> std::size_t first(const BS &bits) {
  if constexpr (requires { bits._Find_first(); }) return bits._Find_first();
  else if constexpr (requires { bits.find_first(); }) return bits.find_first();
  else {
    for (std::size_t i = 0; i < bits.size(); ++i) if (bits[i]) return i;
    return bits.size();
  }
}

template <class BS> std::size_t next(const BS &bits, std::size_t pos) {
  if constexpr (requires { bits._Find_next(pos); }) return bits._Find_next(pos);
  else if constexpr (requires { bits.find_next(pos); }) return bits.find_next(pos + 1);
  else {
    for (++pos; pos < bits.size(); ++pos) if (bits[pos]) return pos;
    return bits.size();
  }
}

} // namespace bipartite_matching_internal

/// @brief Dense bipartite matching with bitset augmenting paths. Adjacency is
/// borrowed and unchanged; it must outlive this object. The first N1 rows have
/// equal capacities >= N2. Bits at positions >= N2 are ignored.
/// Supports GNU/libc++ std::bitset and noya::dynamic_bitset; only GNU and the
/// dynamic bitset provide word-efficient set-bit search.
template <typename BS> struct BipartiteMatching_Dense {
  int N1, N2;
  std::vector<BS> &adj;
  std::vector<int> m1, m2, que, pre;
  BS vis;

  BipartiteMatching_Dense(std::vector<BS> &adj_, int n, int m)
      : N1(n), N2(m), adj(adj_), m1(n, -1), m2(m, -1),
        vis(adj_.empty() ? BS{} : adj_.front()) {
    assert(n >= 0 && m >= 0 && adj.size() >= std::size_t(n));
    for (int u = 0; u < n; ++u)
      assert(adj[u].size() == vis.size() && adj[u].size() >= std::size_t(m));
    using bipartite_matching_internal::first;
    BS free = vis;
    free.set();
    std::vector<int> pending;
    int count = 0, target = std::min(n, m);
    for (int u = 0; u < n && count < target; ++u) {
      if (first(adj[u]) >= std::size_t(m)) continue;
      BS can = adj[u] & free;
      std::size_t v = first(can);
      if (v < std::size_t(m)) {
        m1[u] = int(v);
        m2[v] = u;
        free.reset(v);
        ++count;
      } else {
        pending.push_back(u);
      }
    }
    BS available = vis;
    available.set();
    for (int s : pending) {
      if (count == target) break;
      count += augment(s, available);
    }
  }

  /// @brief Try one augmentation from an unmatched left vertex. After adding
  /// edges, call this for every unmatched left vertex to restore maximal size.
  /// The current matching must remain valid; graph dimensions cannot change.
  void bfs(int s) {
    assert(0 <= s && s < N1);
    if (m1[s] != -1) return;
    BS available = vis;
    available.set(); // Do not reuse failed-search pruning after graph edits.
    augment(s, available);
  }

  /// @brief Return matched pairs in increasing left-vertex order.
  std::vector<std::pair<int, int>> matching() const {
    std::vector<std::pair<int, int>> result;
    for (int u = 0; u < N1; ++u)
      if (m1[u] != -1) result.emplace_back(u, m1[u]);
    return result;
  }

  /// @brief Minimum vertex cover as two increasing vertex lists. Requires a
  /// maximum matching; does not change matching arrays or input adjacency.
  std::pair<std::vector<int>, std::vector<int>> vertex_cover() {
    using namespace bipartite_matching_internal;
    std::vector<int> queue;
    std::vector<bool> reached(N1);
    for (int u = 0; u < N1; ++u)
      if (m1[u] == -1) { reached[u] = true; queue.push_back(u); }
    vis.set();
    for (std::size_t head = 0; head < queue.size(); ++head) {
      BS can = adj[queue[head]] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        int u = m2[v];
        assert(u != -1);
        if (!reached[u]) { reached[u] = true; queue.push_back(u); }
      }
    }
    std::vector<int> left, right;
    for (int u = 0; u < N1; ++u) if (!reached[u]) left.push_back(u);
    for (int v = 0; v < N2; ++v)
      if (m2[v] != -1 && reached[m2[v]]) right.push_back(v);
    return {std::move(left), std::move(right)};
  }

private:
  bool augment(int s, BS &available) {
    using namespace bipartite_matching_internal;
    que.resize(N1);
    pre.resize(N1);
    vis = available;
    pre[s] = -1;
    int head = 0, tail = 0;
    que[tail++] = s;
    while (head < tail) {
      int u = que[head++];
      BS can = adj[u] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        if (m2[v] != -1) {
          int to = m2[v];
          que[tail++] = to;
          pre[to] = u;
          continue;
        }
        int a = u, b = int(v);
        while (a != -1) {
          int previous = m1[a];
          m1[a] = b;
          m2[b] = a;
          a = pre[a];
          b = previous;
        }
        return true;
      }
    }
    // The failed reachable region is closed and has no free right vertex.
    // Later augmentations cannot enter and leave it, so it stays unusable
    // while this constructor's graph is fixed. Keep it excluded across roots.
    available = vis;
    return false;
  }
};

/// @brief Maximum bipartite matching; l/r contain partners or -1, ans is its
/// size. Duplicate edges are allowed. Input order is preserved, unlike the old
/// shuffled implementation; the optimal witness is not canonical.
struct HopcroftKarp {
  std::vector<int> g, l, r;
  int ans = 0;

  HopcroftKarp(int n, int m, const std::vector<std::pair<int, int>> &edges)
      : g(edges.size()), l(n, -1), r(m, -1) {
    assert(n >= 0 && m >= 0);
    std::vector<std::size_t> off(std::size_t(n) + 1);
    for (auto [u, v] : edges) {
      assert(0 <= u && u < n && 0 <= v && v < m);
      ++off[u + 1];
    }
    for (int u = 0; u < n; ++u) off[u + 1] += off[u];
    auto cur = off;
    for (auto [u, v] : edges) g[cur[u]++] = v;
    std::vector<int> active;
    for (int u = 0; u < n; ++u) {
      if (off[u] == off[u + 1]) continue;
      active.push_back(u);
      for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
        int v = g[i];
        if (r[v] == -1) { l[u] = v; r[v] = u; ++ans; break; }
      }
    }
    int target = std::min(int(active.size()), m);
    if (ans == target) return;
    std::vector<int> dist(n, -1), queue;
    queue.reserve(active.size());
    while (ans < target) {
      for (int u : active) dist[u] = -1;
      queue.clear();
      for (int u : active)
        if (l[u] == -1) { dist[u] = 0; queue.push_back(u); }
      bool found = false;
      for (std::size_t head = 0; head < queue.size(); ++head) {
        int u = queue[head];
        for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
          int to = r[g[i]];
          if (to == -1) found = true;
          else if (dist[to] == -1) {
            dist[to] = dist[u] + 1;
            queue.push_back(to);
          }
        }
      }
      if (!found) break;
      // Consume each left vertex once per phase, including successful paths.
      auto dfs = [&](auto &self, int u) -> bool {
        int depth = dist[u];
        dist[u] = -1;
        for (auto i = off[u]; i < off[u + 1]; ++i) {
          int v = g[i], to = r[v];
          if (to == -1 || (dist[to] == depth + 1 && self(self, to))) {
            l[u] = v;
            r[v] = u;
            return true;
          }
        }
        return false;
      };
      for (int u : active)
        if (l[u] == -1 && dfs(dfs, u)) ++ans;
    }
  }
};

} // namespace noya

 // NOYA_BIPARTITE_MATCHING_HPP
#ifndef NOYA_BIPARTITE_MATCHING_HPP
#define NOYA_BIPARTITE_MATCHING_HPP 1

/// @complexity Time: O(V + E sqrt(V)) sparse;
/// O(V + W + N1 * (N1 * W + N2)) dense, with W words per row.
/// Space: O(V + E) sparse; O(V + W) dense auxiliary plus O(N1 * W) adjacency.

#include <algorithm>
#include <bitset>
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_matching_internal {

// GNU bitset and noya::dynamic_bitset have different find-next conventions.
// Other bitsets (including libc++ std::bitset) use a portable bitwise scan.
// For that fallback, replace W by the row's bit capacity in the time bound.
// dynamic_bitset is supplied by noya/ds/dynamic_bitset.hpp when needed.
template <class BS> std::size_t first(const BS &bits) {
  if constexpr (requires { bits._Find_first(); }) return bits._Find_first();
  else if constexpr (requires { bits.find_first(); }) return bits.find_first();
  else {
    for (std::size_t i = 0; i < bits.size(); ++i) if (bits[i]) return i;
    return bits.size();
  }
}

template <class BS> std::size_t next(const BS &bits, std::size_t pos) {
  if constexpr (requires { bits._Find_next(pos); }) return bits._Find_next(pos);
  else if constexpr (requires { bits.find_next(pos); }) return bits.find_next(pos + 1);
  else {
    for (++pos; pos < bits.size(); ++pos) if (bits[pos]) return pos;
    return bits.size();
  }
}

} // namespace bipartite_matching_internal

/// @brief Dense bipartite matching with bitset augmenting paths. Adjacency is
/// borrowed and unchanged; it must outlive this object. The first N1 rows have
/// equal capacities >= N2. Bits at positions >= N2 are ignored.
/// Supports GNU/libc++ std::bitset and noya::dynamic_bitset; only GNU and the
/// dynamic bitset provide word-efficient set-bit search.
template <typename BS> struct BipartiteMatching_Dense {
  int N1, N2;
  std::vector<BS> &adj;
  std::vector<int> m1, m2, que, pre;
  BS vis;

  BipartiteMatching_Dense(std::vector<BS> &adj_, int n, int m)
      : N1(n), N2(m), adj(adj_), m1(n, -1), m2(m, -1),
        vis(adj_.empty() ? BS{} : adj_.front()) {
    assert(n >= 0 && m >= 0 && adj.size() >= std::size_t(n));
    for (int u = 0; u < n; ++u)
      assert(adj[u].size() == vis.size() && adj[u].size() >= std::size_t(m));
    using bipartite_matching_internal::first;
    BS free = vis;
    free.set();
    std::vector<int> pending;
    int count = 0, target = std::min(n, m);
    for (int u = 0; u < n && count < target; ++u) {
      if (first(adj[u]) >= std::size_t(m)) continue;
      BS can = adj[u] & free;
      std::size_t v = first(can);
      if (v < std::size_t(m)) {
        m1[u] = int(v);
        m2[v] = u;
        free.reset(v);
        ++count;
      } else {
        pending.push_back(u);
      }
    }
    BS available = vis;
    available.set();
    for (int s : pending) {
      if (count == target) break;
      count += augment(s, available);
    }
  }

  /// @brief Try one augmentation from an unmatched left vertex. After adding
  /// edges, call this for every unmatched left vertex to restore maximal size.
  /// The current matching must remain valid; graph dimensions cannot change.
  void bfs(int s) {
    assert(0 <= s && s < N1);
    if (m1[s] != -1) return;
    BS available = vis;
    available.set(); // Do not reuse failed-search pruning after graph edits.
    augment(s, available);
  }

  /// @brief Return matched pairs in increasing left-vertex order.
  std::vector<std::pair<int, int>> matching() const {
    std::vector<std::pair<int, int>> result;
    for (int u = 0; u < N1; ++u)
      if (m1[u] != -1) result.emplace_back(u, m1[u]);
    return result;
  }

  /// @brief Minimum vertex cover as two increasing vertex lists. Requires a
  /// maximum matching; does not change matching arrays or input adjacency.
  std::pair<std::vector<int>, std::vector<int>> vertex_cover() {
    using namespace bipartite_matching_internal;
    std::vector<int> queue;
    std::vector<bool> reached(N1);
    for (int u = 0; u < N1; ++u)
      if (m1[u] == -1) { reached[u] = true; queue.push_back(u); }
    vis.set();
    for (std::size_t head = 0; head < queue.size(); ++head) {
      BS can = adj[queue[head]] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        int u = m2[v];
        assert(u != -1);
        if (!reached[u]) { reached[u] = true; queue.push_back(u); }
      }
    }
    std::vector<int> left, right;
    for (int u = 0; u < N1; ++u) if (!reached[u]) left.push_back(u);
    for (int v = 0; v < N2; ++v)
      if (m2[v] != -1 && reached[m2[v]]) right.push_back(v);
    return {std::move(left), std::move(right)};
  }

private:
  bool augment(int s, BS &available) {
    using namespace bipartite_matching_internal;
    que.resize(N1);
    pre.resize(N1);
    vis = available;
    pre[s] = -1;
    int head = 0, tail = 0;
    que[tail++] = s;
    while (head < tail) {
      int u = que[head++];
      BS can = adj[u] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        if (m2[v] != -1) {
          int to = m2[v];
          que[tail++] = to;
          pre[to] = u;
          continue;
        }
        int a = u, b = int(v);
        while (a != -1) {
          int previous = m1[a];
          m1[a] = b;
          m2[b] = a;
          a = pre[a];
          b = previous;
        }
        return true;
      }
    }
    // The failed reachable region is closed and has no free right vertex.
    // Later augmentations cannot enter and leave it, so it stays unusable
    // while this constructor's graph is fixed. Keep it excluded across roots.
    available = vis;
    return false;
  }
};

/// @brief Maximum bipartite matching; l/r contain partners or -1, ans is its
/// size. Duplicate edges are allowed. Input order is preserved, unlike the old
/// shuffled implementation; the optimal witness is not canonical.
struct HopcroftKarp {
  std::vector<int> g, l, r;
  int ans = 0;

  HopcroftKarp(int n, int m, const std::vector<std::pair<int, int>> &edges)
      : g(edges.size()), l(n, -1), r(m, -1) {
    assert(n >= 0 && m >= 0);
    std::vector<std::size_t> off(std::size_t(n) + 1);
    for (auto [u, v] : edges) {
      assert(0 <= u && u < n && 0 <= v && v < m);
      ++off[u + 1];
    }
    for (int u = 0; u < n; ++u) off[u + 1] += off[u];
    auto cur = off;
    for (auto [u, v] : edges) g[cur[u]++] = v;
    std::vector<int> active;
    for (int u = 0; u < n; ++u) {
      if (off[u] == off[u + 1]) continue;
      active.push_back(u);
      for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
        int v = g[i];
        if (r[v] == -1) { l[u] = v; r[v] = u; ++ans; break; }
      }
    }
    int target = std::min(int(active.size()), m);
    if (ans == target) return;
    std::vector<int> dist(n, -1), queue;
    queue.reserve(active.size());
    while (ans < target) {
      for (int u : active) dist[u] = -1;
      queue.clear();
      for (int u : active)
        if (l[u] == -1) { dist[u] = 0; queue.push_back(u); }
      bool found = false;
      for (std::size_t head = 0; head < queue.size(); ++head) {
        int u = queue[head];
        for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
          int to = r[g[i]];
          if (to == -1) found = true;
          else if (dist[to] == -1) {
            dist[to] = dist[u] + 1;
            queue.push_back(to);
          }
        }
      }
      if (!found) break;
      // Consume each left vertex once per phase, including successful paths.
      auto dfs = [&](auto &self, int u) -> bool {
        int depth = dist[u];
        dist[u] = -1;
        for (auto i = off[u]; i < off[u + 1]; ++i) {
          int v = g[i], to = r[v];
          if (to == -1 || (dist[to] == depth + 1 && self(self, to))) {
            l[u] = v;
            r[v] = u;
            return true;
          }
        }
        return false;
      };
      for (int u : active)
        if (l[u] == -1 && dfs(dfs, u)) ++ans;
    }
  }
};

} // namespace noya

#endif // NOYA_BIPARTITE_MATCHING_HPP
/// @complexity Time: O(V + E sqrt(V)) sparse;
/// O(V + W + N1 * (N1 * W + N2)) dense, with W words per row.
/// Space: O(V + E) sparse; O(V + W) dense auxiliary plus O(N1 * W) adjacency.

#include <algorithm>
#include <bitset>
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_matching_internal {

// GNU bitset and noya::dynamic_bitset have different find-next conventions.
// Other bitsets (including libc++ std::bitset) use a portable bitwise scan.
// For that fallback, replace W by the row's bit capacity in the time bound.
// dynamic_bitset is supplied by noya/ds/dynamic_bitset.hpp when needed.
template <class BS> std::size_t first(const BS &bits) {
  if constexpr (requires { bits._Find_first(); }) return bits._Find_first();
  else if constexpr (requires { bits.find_first(); }) return bits.find_first();
  else {
    for (std::size_t i = 0; i < bits.size(); ++i) if (bits[i]) return i;
    return bits.size();
  }
}

template <class BS> std::size_t next(const BS &bits, std::size_t pos) {
  if constexpr (requires { bits._Find_next(pos); }) return bits._Find_next(pos);
  else if constexpr (requires { bits.find_next(pos); }) return bits.find_next(pos + 1);
  else {
    for (++pos; pos < bits.size(); ++pos) if (bits[pos]) return pos;
    return bits.size();
  }
}

} // namespace bipartite_matching_internal

/// @brief Dense bipartite matching with bitset augmenting paths. Adjacency is
/// borrowed and unchanged; it must outlive this object. The first N1 rows have
/// equal capacities >= N2. Bits at positions >= N2 are ignored.
/// Supports GNU/libc++ std::bitset and noya::dynamic_bitset; only GNU and the
/// dynamic bitset provide word-efficient set-bit search.
template <typename BS> struct BipartiteMatching_Dense {
  int N1, N2;
  std::vector<BS> &adj;
  std::vector<int> m1, m2, que, pre;
  BS vis;

  BipartiteMatching_Dense(std::vector<BS> &adj_, int n, int m)
      : N1(n), N2(m), adj(adj_), m1(n, -1), m2(m, -1),
        vis(adj_.empty() ? BS{} : adj_.front()) {
    assert(n >= 0 && m >= 0 && adj.size() >= std::size_t(n));
    for (int u = 0; u < n; ++u)
      assert(adj[u].size() == vis.size() && adj[u].size() >= std::size_t(m));
    using bipartite_matching_internal::first;
    BS free = vis;
    free.set();
    std::vector<int> pending;
    int count = 0, target = std::min(n, m);
    for (int u = 0; u < n && count < target; ++u) {
      if (first(adj[u]) >= std::size_t(m)) continue;
      BS can = adj[u] & free;
      std::size_t v = first(can);
      if (v < std::size_t(m)) {
        m1[u] = int(v);
        m2[v] = u;
        free.reset(v);
        ++count;
      } else {
        pending.push_back(u);
      }
    }
    BS available = vis;
    available.set();
    for (int s : pending) {
      if (count == target) break;
      count += augment(s, available);
    }
  }

  /// @brief Try one augmentation from an unmatched left vertex. After adding
  /// edges, call this for every unmatched left vertex to restore maximal size.
  /// The current matching must remain valid; graph dimensions cannot change.
  void bfs(int s) {
    assert(0 <= s && s < N1);
    if (m1[s] != -1) return;
    BS available = vis;
    available.set(); // Do not reuse failed-search pruning after graph edits.
    augment(s, available);
  }

  /// @brief Return matched pairs in increasing left-vertex order.
  std::vector<std::pair<int, int>> matching() const {
    std::vector<std::pair<int, int>> result;
    for (int u = 0; u < N1; ++u)
      if (m1[u] != -1) result.emplace_back(u, m1[u]);
    return result;
  }

  /// @brief Minimum vertex cover as two increasing vertex lists. Requires a
  /// maximum matching; does not change matching arrays or input adjacency.
  std::pair<std::vector<int>, std::vector<int>> vertex_cover() {
    using namespace bipartite_matching_internal;
    std::vector<int> queue;
    std::vector<bool> reached(N1);
    for (int u = 0; u < N1; ++u)
      if (m1[u] == -1) { reached[u] = true; queue.push_back(u); }
    vis.set();
    for (std::size_t head = 0; head < queue.size(); ++head) {
      BS can = adj[queue[head]] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        int u = m2[v];
        assert(u != -1);
        if (!reached[u]) { reached[u] = true; queue.push_back(u); }
      }
    }
    std::vector<int> left, right;
    for (int u = 0; u < N1; ++u) if (!reached[u]) left.push_back(u);
    for (int v = 0; v < N2; ++v)
      if (m2[v] != -1 && reached[m2[v]]) right.push_back(v);
    return {std::move(left), std::move(right)};
  }

private:
  bool augment(int s, BS &available) {
    using namespace bipartite_matching_internal;
    que.resize(N1);
    pre.resize(N1);
    vis = available;
    pre[s] = -1;
    int head = 0, tail = 0;
    que[tail++] = s;
    while (head < tail) {
      int u = que[head++];
      BS can = adj[u] & vis;
      for (std::size_t v = first(can); v < std::size_t(N2); v = next(can, v)) {
        vis.reset(v);
        if (m2[v] != -1) {
          int to = m2[v];
          que[tail++] = to;
          pre[to] = u;
          continue;
        }
        int a = u, b = int(v);
        while (a != -1) {
          int previous = m1[a];
          m1[a] = b;
          m2[b] = a;
          a = pre[a];
          b = previous;
        }
        return true;
      }
    }
    // The failed reachable region is closed and has no free right vertex.
    // Later augmentations cannot enter and leave it, so it stays unusable
    // while this constructor's graph is fixed. Keep it excluded across roots.
    available = vis;
    return false;
  }
};

/// @brief Maximum bipartite matching; l/r contain partners or -1, ans is its
/// size. Duplicate edges are allowed. Input order is preserved, unlike the old
/// shuffled implementation; the optimal witness is not canonical.
struct HopcroftKarp {
  std::vector<int> g, l, r;
  int ans = 0;

  HopcroftKarp(int n, int m, const std::vector<std::pair<int, int>> &edges)
      : g(edges.size()), l(n, -1), r(m, -1) {
    assert(n >= 0 && m >= 0);
    std::vector<std::size_t> off(std::size_t(n) + 1);
    for (auto [u, v] : edges) {
      assert(0 <= u && u < n && 0 <= v && v < m);
      ++off[u + 1];
    }
    for (int u = 0; u < n; ++u) off[u + 1] += off[u];
    auto cur = off;
    for (auto [u, v] : edges) g[cur[u]++] = v;
    std::vector<int> active;
    for (int u = 0; u < n; ++u) {
      if (off[u] == off[u + 1]) continue;
      active.push_back(u);
      for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
        int v = g[i];
        if (r[v] == -1) { l[u] = v; r[v] = u; ++ans; break; }
      }
    }
    int target = std::min(int(active.size()), m);
    if (ans == target) return;
    std::vector<int> dist(n, -1), queue;
    queue.reserve(active.size());
    while (ans < target) {
      for (int u : active) dist[u] = -1;
      queue.clear();
      for (int u : active)
        if (l[u] == -1) { dist[u] = 0; queue.push_back(u); }
      bool found = false;
      for (std::size_t head = 0; head < queue.size(); ++head) {
        int u = queue[head];
        for (std::size_t i = off[u]; i < off[u + 1]; ++i) {
          int to = r[g[i]];
          if (to == -1) found = true;
          else if (dist[to] == -1) {
            dist[to] = dist[u] + 1;
            queue.push_back(to);
          }
        }
      }
      if (!found) break;
      // Consume each left vertex once per phase, including successful paths.
      auto dfs = [&](auto &self, int u) -> bool {
        int depth = dist[u];
        dist[u] = -1;
        for (auto i = off[u]; i < off[u + 1]; ++i) {
          int v = g[i], to = r[v];
          if (to == -1 || (dist[to] == depth + 1 && self(self, to))) {
            l[u] = v;
            r[v] = u;
            return true;
          }
        }
        return false;
      };
      for (int u : active)
        if (l[u] == -1 && dfs(dfs, u)) ++ans;
    }
  }
};

} // namespace noya

 // NOYA_BIPARTITE_MATCHING_HPP