Skip to content

bipartite_edge_coloring.hpp

SECTIONGraph INCLUDEnoya/graph/bipartite_edge_coloring.hpp

用最大度种颜色给二分图的边染色,使同一顶点上的边颜色互异;按输入边顺序返回颜色,支持重边。

Complexity: Time: \(O(V + E \sqrt{S} \log (\Delta + 1))\); Space: \(O(V + E)\), where \(S\) is the larger contracted side and \(S \cdot \Delta \le 2 E\).

AC 记录:bipartite_edge_coloring。

跳到代码 · GitHub ↗

Implementation

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

/// @complexity Time: O(V + E sqrt(S) log(Delta + 1)); Space: O(V + E),
/// where S is the larger contracted side and S * Delta <= 2E.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_edge_coloring_internal {

inline std::pair<std::vector<int>, int>
contract_vertices(const std::vector<int> &deg, int delta) {
  // Counting-sort positive degrees, then pack consecutive vertices. Any two
  // output groups have combined degree > delta, so padding needs at most 2E
  // edges. Isolated vertices need no group.
  std::vector<int> end(std::size_t(delta) + 1);
  for (int d : deg) if (d) ++end[d];
  for (int d = 1; d <= delta; ++d) end[d] += end[d - 1];
  std::vector<int> order(end.back()), map(deg.size(), -1);
  for (int u = int(deg.size()); u-- > 0;)
    if (deg[u]) order[--end[deg[u]]] = u;
  int group = 0, sum = 0;
  for (int u : order) {
    if (deg[u] > delta - sum) {
      ++group;
      sum = 0;
    }
    map[u] = group;
    sum += deg[u];
  }
  return {std::move(map), order.empty() ? 0 : group + 1};
}

struct edge_record {
  int l, r, id1;
};

inline std::vector<int> perfect_matching(int n,
                                         const std::vector<edge_record> &es,
                                         const std::vector<int> &ids) {
  // Every row has the same degree, so CSR offsets are just u * degree.
  const std::size_t degree = ids.size() / n;
  std::vector<int> adj(ids.size()), ml(n, -1), mr(n, -1), dist(n), queue;
  std::vector<std::size_t> cur(n);
  for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
  for (int id : ids) adj[cur[es[id].l]++] = id;
  int count = 0;
  for (int u = 0; u < n; ++u) {
    for (std::size_t i = std::size_t(u) * degree;
         i < (std::size_t(u) + 1) * degree; ++i) {
      int id = adj[i], r = es[id].r;
      if (mr[r] == -1) {
        ml[u] = mr[r] = id;
        ++count;
        break;
      }
    }
  }

  queue.reserve(n);
  while (count < n) {
    std::fill(dist.begin(), dist.end(), -1);
    queue.clear();
    for (int u = 0; u < n; ++u) {
      if (ml[u] == -1) {
        dist[u] = 0;
        queue.push_back(u);
      }
    }
    int limit = -1;
    for (std::size_t head = 0; head < queue.size(); ++head) {
      int u = queue[head];
      if (limit >= 0 && dist[u] >= limit) continue;
      for (std::size_t i = std::size_t(u) * degree;
           i < (std::size_t(u) + 1) * degree; ++i) {
        int id = mr[es[adj[i]].r];
        if (id == -1) {
          limit = dist[u] + 1;
        } else {
          int v = es[id].l;
          if (dist[v] == -1) {
            dist[v] = dist[u] + 1;
            queue.push_back(v);
          }
        }
      }
    }
    assert(limit > 0); // A nonempty regular bipartite graph has a perfect match.
    for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
    auto dfs = [&](auto &self, int u) -> bool {
      while (cur[u] < (std::size_t(u) + 1) * degree) {
        int id = adj[cur[u]++], old = mr[es[id].r];
        int v = old == -1 ? -1 : es[old].l;
        if (v == -1 ? dist[u] + 1 == limit
                    : dist[v] == dist[u] + 1 && dist[v] < limit && self(self, v)) {
          ml[u] = mr[es[id].r] = id;
          return true;
        }
      }
      dist[u] = -1;
      return false;
    };
    for (int u = 0; u < n; ++u)
      if (ml[u] == -1 && dfs(dfs, u)) ++count;
  }
  return ml;
}

struct coloring_context {
  int n;
  const std::vector<edge_record> &es;
  std::vector<int> &answer;
  std::vector<unsigned char> used;

  coloring_context(int n_, const std::vector<edge_record> &es_,
                   std::vector<int> &answer_)
      : n(n_), es(es_), answer(answer_), used(es_.size()) {}

  std::vector<int> split_even_regular(std::vector<int> &ids, int degree) {
    std::vector<int> adj(2 * ids.size()), other;
    std::vector<std::size_t> ptr(std::size_t(n) * 2);
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    for (int id : ids) {
      used[id] = 0; // Reset only this subproblem, not the entire root graph.
      adj[ptr[es[id].l]++] = id;
      adj[ptr[std::size_t(n) + es[id].r]++] = id;
    }
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    other.reserve(ids.size() / 2);
    std::size_t count = 0;
    for (std::size_t s = 0; s < ptr.size(); ++s) {
      // A maximal trail in an even-degree graph closes at its start. Because
      // the graph is bipartite, its length is even: alternate edges directly.
      std::size_t u = s;
      bool parity = false;
      while (true) {
        while (ptr[u] < (u + 1) * degree && used[adj[ptr[u]]]) ++ptr[u];
        if (ptr[u] == (u + 1) * degree) break;
        int id = adj[ptr[u]++];
        used[id] = 1;
        if (parity) other.push_back(id);
        else ids[count++] = id;
        parity = !parity;
        u = u < std::size_t(n) ? std::size_t(n) + es[id].r : es[id].l;
      }
      assert(u == s && !parity);
    }
    assert(count == ids.size() / 2 && other.size() == count);
    ids.resize(count);
    return other;
  }

  void assign(const std::vector<int> &ids, int color) {
    for (int id : ids)
      if (es[id].id1 != -1) answer[es[id].id1] = color;
  }

  void solve(std::vector<int> ids, int degree, int offset = 0) {
    assert(degree > 0 && ids.size() == std::size_t(n) * degree);
    if (degree == 1) {
      assign(ids, offset);
    } else if (degree % 2 == 0) {
      auto other = split_even_regular(ids, degree);
      solve(std::move(ids), degree / 2, offset);
      solve(std::move(other), degree / 2, offset + degree / 2);
    } else {
      auto matching = perfect_matching(n, es, ids);
      assign(matching, offset);
      std::size_t count = 0;
      for (int id : ids)
        if (matching[es[id].l] != id) ids[count++] = id;
      ids.resize(count);
      solve(std::move(ids), degree - 1, offset + 1);
    }
  }
};

} // namespace bipartite_edge_coloring_internal

/// @brief Color a bipartite multigraph optimally using colors [0, Delta).
/// Returns colors in input-edge order; duplicate edges are distinct. The input
/// is unchanged, but the optimal coloring is not canonical. Empty input gives
/// an empty result. Original and padded edge counts must fit in int.
inline std::vector<int>
bipartite_edge_coloring(int nl, int nr,
                        const std::vector<std::pair<int, int>> &input) {
  using namespace bipartite_edge_coloring_internal;
  assert(nl >= 0 && nr >= 0);
  assert(input.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (input.empty()) return {};
  std::vector<int> dl(nl), dr(nr);
  int delta = 0;
  for (auto [l, r] : input) {
    assert(0 <= l && l < nl && 0 <= r && r < nr);
    delta = std::max(delta, ++dl[l]);
    delta = std::max(delta, ++dr[r]);
  }
  std::vector<int> answer(input.size());
  if (delta == 1) return answer;
  if (std::size_t(delta) == input.size()) {
    std::iota(answer.begin(), answer.end(), 0);
    return answer;
  }

  auto [li, nl1] = contract_vertices(dl, delta);
  auto [ri, nr1] = contract_vertices(dr, delta);
  int n = std::max(nl1, nr1);
  std::size_t padded = std::size_t(n) * delta;
  assert(padded <= 2 * input.size());
  assert(padded <= std::size_t(std::numeric_limits<int>::max()));
  std::vector<int> left(n), right(n);
  std::vector<edge_record> es;
  es.reserve(padded);
  for (int id = 0; id < int(input.size()); ++id) {
    int l = li[input[id].first], r = ri[input[id].second];
    es.push_back({l, r, id});
    ++left[l];
    ++right[r];
  }
  int l = 0, r = 0;
  while (l < n && r < n) {
    while (l < n && left[l] == delta) ++l;
    while (r < n && right[r] == delta) ++r;
    if (l == n || r == n) break;
    int count = std::min(delta - left[l], delta - right[r]);
    for (int k = 0; k < count; ++k) es.push_back({l, r, -1});
    left[l] += count;
    right[r] += count;
  }
  assert(es.size() == padded);
  std::vector<int> ids(es.size());
  std::iota(ids.begin(), ids.end(), 0);
  coloring_context work(n, es, answer);
  work.solve(std::move(ids), delta);
  return answer;
}

} // namespace noya

 // NOYA_BIPARTITE_EDGE_COLORING_HPP
#ifndef NOYA_BIPARTITE_EDGE_COLORING_HPP
#define NOYA_BIPARTITE_EDGE_COLORING_HPP 1

/// @complexity Time: O(V + E sqrt(S) log(Delta + 1)); Space: O(V + E),
/// where S is the larger contracted side and S * Delta <= 2E.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_edge_coloring_internal {

inline std::pair<std::vector<int>, int>
contract_vertices(const std::vector<int> &deg, int delta) {
  // Counting-sort positive degrees, then pack consecutive vertices. Any two
  // output groups have combined degree > delta, so padding needs at most 2E
  // edges. Isolated vertices need no group.
  std::vector<int> end(std::size_t(delta) + 1);
  for (int d : deg) if (d) ++end[d];
  for (int d = 1; d <= delta; ++d) end[d] += end[d - 1];
  std::vector<int> order(end.back()), map(deg.size(), -1);
  for (int u = int(deg.size()); u-- > 0;)
    if (deg[u]) order[--end[deg[u]]] = u;
  int group = 0, sum = 0;
  for (int u : order) {
    if (deg[u] > delta - sum) {
      ++group;
      sum = 0;
    }
    map[u] = group;
    sum += deg[u];
  }
  return {std::move(map), order.empty() ? 0 : group + 1};
}

struct edge_record {
  int l, r, id1;
};

inline std::vector<int> perfect_matching(int n,
                                         const std::vector<edge_record> &es,
                                         const std::vector<int> &ids) {
  // Every row has the same degree, so CSR offsets are just u * degree.
  const std::size_t degree = ids.size() / n;
  std::vector<int> adj(ids.size()), ml(n, -1), mr(n, -1), dist(n), queue;
  std::vector<std::size_t> cur(n);
  for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
  for (int id : ids) adj[cur[es[id].l]++] = id;
  int count = 0;
  for (int u = 0; u < n; ++u) {
    for (std::size_t i = std::size_t(u) * degree;
         i < (std::size_t(u) + 1) * degree; ++i) {
      int id = adj[i], r = es[id].r;
      if (mr[r] == -1) {
        ml[u] = mr[r] = id;
        ++count;
        break;
      }
    }
  }

  queue.reserve(n);
  while (count < n) {
    std::fill(dist.begin(), dist.end(), -1);
    queue.clear();
    for (int u = 0; u < n; ++u) {
      if (ml[u] == -1) {
        dist[u] = 0;
        queue.push_back(u);
      }
    }
    int limit = -1;
    for (std::size_t head = 0; head < queue.size(); ++head) {
      int u = queue[head];
      if (limit >= 0 && dist[u] >= limit) continue;
      for (std::size_t i = std::size_t(u) * degree;
           i < (std::size_t(u) + 1) * degree; ++i) {
        int id = mr[es[adj[i]].r];
        if (id == -1) {
          limit = dist[u] + 1;
        } else {
          int v = es[id].l;
          if (dist[v] == -1) {
            dist[v] = dist[u] + 1;
            queue.push_back(v);
          }
        }
      }
    }
    assert(limit > 0); // A nonempty regular bipartite graph has a perfect match.
    for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
    auto dfs = [&](auto &self, int u) -> bool {
      while (cur[u] < (std::size_t(u) + 1) * degree) {
        int id = adj[cur[u]++], old = mr[es[id].r];
        int v = old == -1 ? -1 : es[old].l;
        if (v == -1 ? dist[u] + 1 == limit
                    : dist[v] == dist[u] + 1 && dist[v] < limit && self(self, v)) {
          ml[u] = mr[es[id].r] = id;
          return true;
        }
      }
      dist[u] = -1;
      return false;
    };
    for (int u = 0; u < n; ++u)
      if (ml[u] == -1 && dfs(dfs, u)) ++count;
  }
  return ml;
}

struct coloring_context {
  int n;
  const std::vector<edge_record> &es;
  std::vector<int> &answer;
  std::vector<unsigned char> used;

  coloring_context(int n_, const std::vector<edge_record> &es_,
                   std::vector<int> &answer_)
      : n(n_), es(es_), answer(answer_), used(es_.size()) {}

  std::vector<int> split_even_regular(std::vector<int> &ids, int degree) {
    std::vector<int> adj(2 * ids.size()), other;
    std::vector<std::size_t> ptr(std::size_t(n) * 2);
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    for (int id : ids) {
      used[id] = 0; // Reset only this subproblem, not the entire root graph.
      adj[ptr[es[id].l]++] = id;
      adj[ptr[std::size_t(n) + es[id].r]++] = id;
    }
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    other.reserve(ids.size() / 2);
    std::size_t count = 0;
    for (std::size_t s = 0; s < ptr.size(); ++s) {
      // A maximal trail in an even-degree graph closes at its start. Because
      // the graph is bipartite, its length is even: alternate edges directly.
      std::size_t u = s;
      bool parity = false;
      while (true) {
        while (ptr[u] < (u + 1) * degree && used[adj[ptr[u]]]) ++ptr[u];
        if (ptr[u] == (u + 1) * degree) break;
        int id = adj[ptr[u]++];
        used[id] = 1;
        if (parity) other.push_back(id);
        else ids[count++] = id;
        parity = !parity;
        u = u < std::size_t(n) ? std::size_t(n) + es[id].r : es[id].l;
      }
      assert(u == s && !parity);
    }
    assert(count == ids.size() / 2 && other.size() == count);
    ids.resize(count);
    return other;
  }

  void assign(const std::vector<int> &ids, int color) {
    for (int id : ids)
      if (es[id].id1 != -1) answer[es[id].id1] = color;
  }

  void solve(std::vector<int> ids, int degree, int offset = 0) {
    assert(degree > 0 && ids.size() == std::size_t(n) * degree);
    if (degree == 1) {
      assign(ids, offset);
    } else if (degree % 2 == 0) {
      auto other = split_even_regular(ids, degree);
      solve(std::move(ids), degree / 2, offset);
      solve(std::move(other), degree / 2, offset + degree / 2);
    } else {
      auto matching = perfect_matching(n, es, ids);
      assign(matching, offset);
      std::size_t count = 0;
      for (int id : ids)
        if (matching[es[id].l] != id) ids[count++] = id;
      ids.resize(count);
      solve(std::move(ids), degree - 1, offset + 1);
    }
  }
};

} // namespace bipartite_edge_coloring_internal

/// @brief Color a bipartite multigraph optimally using colors [0, Delta).
/// Returns colors in input-edge order; duplicate edges are distinct. The input
/// is unchanged, but the optimal coloring is not canonical. Empty input gives
/// an empty result. Original and padded edge counts must fit in int.
inline std::vector<int>
bipartite_edge_coloring(int nl, int nr,
                        const std::vector<std::pair<int, int>> &input) {
  using namespace bipartite_edge_coloring_internal;
  assert(nl >= 0 && nr >= 0);
  assert(input.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (input.empty()) return {};
  std::vector<int> dl(nl), dr(nr);
  int delta = 0;
  for (auto [l, r] : input) {
    assert(0 <= l && l < nl && 0 <= r && r < nr);
    delta = std::max(delta, ++dl[l]);
    delta = std::max(delta, ++dr[r]);
  }
  std::vector<int> answer(input.size());
  if (delta == 1) return answer;
  if (std::size_t(delta) == input.size()) {
    std::iota(answer.begin(), answer.end(), 0);
    return answer;
  }

  auto [li, nl1] = contract_vertices(dl, delta);
  auto [ri, nr1] = contract_vertices(dr, delta);
  int n = std::max(nl1, nr1);
  std::size_t padded = std::size_t(n) * delta;
  assert(padded <= 2 * input.size());
  assert(padded <= std::size_t(std::numeric_limits<int>::max()));
  std::vector<int> left(n), right(n);
  std::vector<edge_record> es;
  es.reserve(padded);
  for (int id = 0; id < int(input.size()); ++id) {
    int l = li[input[id].first], r = ri[input[id].second];
    es.push_back({l, r, id});
    ++left[l];
    ++right[r];
  }
  int l = 0, r = 0;
  while (l < n && r < n) {
    while (l < n && left[l] == delta) ++l;
    while (r < n && right[r] == delta) ++r;
    if (l == n || r == n) break;
    int count = std::min(delta - left[l], delta - right[r]);
    for (int k = 0; k < count; ++k) es.push_back({l, r, -1});
    left[l] += count;
    right[r] += count;
  }
  assert(es.size() == padded);
  std::vector<int> ids(es.size());
  std::iota(ids.begin(), ids.end(), 0);
  coloring_context work(n, es, answer);
  work.solve(std::move(ids), delta);
  return answer;
}

} // namespace noya

#endif // NOYA_BIPARTITE_EDGE_COLORING_HPP
/// @complexity Time: O(V + E sqrt(S) log(Delta + 1)); Space: O(V + E),
/// where S is the larger contracted side and S * Delta <= 2E.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <utility>
#include <vector>

namespace noya {
namespace bipartite_edge_coloring_internal {

inline std::pair<std::vector<int>, int>
contract_vertices(const std::vector<int> &deg, int delta) {
  // Counting-sort positive degrees, then pack consecutive vertices. Any two
  // output groups have combined degree > delta, so padding needs at most 2E
  // edges. Isolated vertices need no group.
  std::vector<int> end(std::size_t(delta) + 1);
  for (int d : deg) if (d) ++end[d];
  for (int d = 1; d <= delta; ++d) end[d] += end[d - 1];
  std::vector<int> order(end.back()), map(deg.size(), -1);
  for (int u = int(deg.size()); u-- > 0;)
    if (deg[u]) order[--end[deg[u]]] = u;
  int group = 0, sum = 0;
  for (int u : order) {
    if (deg[u] > delta - sum) {
      ++group;
      sum = 0;
    }
    map[u] = group;
    sum += deg[u];
  }
  return {std::move(map), order.empty() ? 0 : group + 1};
}

struct edge_record {
  int l, r, id1;
};

inline std::vector<int> perfect_matching(int n,
                                         const std::vector<edge_record> &es,
                                         const std::vector<int> &ids) {
  // Every row has the same degree, so CSR offsets are just u * degree.
  const std::size_t degree = ids.size() / n;
  std::vector<int> adj(ids.size()), ml(n, -1), mr(n, -1), dist(n), queue;
  std::vector<std::size_t> cur(n);
  for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
  for (int id : ids) adj[cur[es[id].l]++] = id;
  int count = 0;
  for (int u = 0; u < n; ++u) {
    for (std::size_t i = std::size_t(u) * degree;
         i < (std::size_t(u) + 1) * degree; ++i) {
      int id = adj[i], r = es[id].r;
      if (mr[r] == -1) {
        ml[u] = mr[r] = id;
        ++count;
        break;
      }
    }
  }

  queue.reserve(n);
  while (count < n) {
    std::fill(dist.begin(), dist.end(), -1);
    queue.clear();
    for (int u = 0; u < n; ++u) {
      if (ml[u] == -1) {
        dist[u] = 0;
        queue.push_back(u);
      }
    }
    int limit = -1;
    for (std::size_t head = 0; head < queue.size(); ++head) {
      int u = queue[head];
      if (limit >= 0 && dist[u] >= limit) continue;
      for (std::size_t i = std::size_t(u) * degree;
           i < (std::size_t(u) + 1) * degree; ++i) {
        int id = mr[es[adj[i]].r];
        if (id == -1) {
          limit = dist[u] + 1;
        } else {
          int v = es[id].l;
          if (dist[v] == -1) {
            dist[v] = dist[u] + 1;
            queue.push_back(v);
          }
        }
      }
    }
    assert(limit > 0); // A nonempty regular bipartite graph has a perfect match.
    for (int u = 0; u < n; ++u) cur[u] = std::size_t(u) * degree;
    auto dfs = [&](auto &self, int u) -> bool {
      while (cur[u] < (std::size_t(u) + 1) * degree) {
        int id = adj[cur[u]++], old = mr[es[id].r];
        int v = old == -1 ? -1 : es[old].l;
        if (v == -1 ? dist[u] + 1 == limit
                    : dist[v] == dist[u] + 1 && dist[v] < limit && self(self, v)) {
          ml[u] = mr[es[id].r] = id;
          return true;
        }
      }
      dist[u] = -1;
      return false;
    };
    for (int u = 0; u < n; ++u)
      if (ml[u] == -1 && dfs(dfs, u)) ++count;
  }
  return ml;
}

struct coloring_context {
  int n;
  const std::vector<edge_record> &es;
  std::vector<int> &answer;
  std::vector<unsigned char> used;

  coloring_context(int n_, const std::vector<edge_record> &es_,
                   std::vector<int> &answer_)
      : n(n_), es(es_), answer(answer_), used(es_.size()) {}

  std::vector<int> split_even_regular(std::vector<int> &ids, int degree) {
    std::vector<int> adj(2 * ids.size()), other;
    std::vector<std::size_t> ptr(std::size_t(n) * 2);
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    for (int id : ids) {
      used[id] = 0; // Reset only this subproblem, not the entire root graph.
      adj[ptr[es[id].l]++] = id;
      adj[ptr[std::size_t(n) + es[id].r]++] = id;
    }
    for (std::size_t u = 0; u < ptr.size(); ++u) ptr[u] = u * degree;
    other.reserve(ids.size() / 2);
    std::size_t count = 0;
    for (std::size_t s = 0; s < ptr.size(); ++s) {
      // A maximal trail in an even-degree graph closes at its start. Because
      // the graph is bipartite, its length is even: alternate edges directly.
      std::size_t u = s;
      bool parity = false;
      while (true) {
        while (ptr[u] < (u + 1) * degree && used[adj[ptr[u]]]) ++ptr[u];
        if (ptr[u] == (u + 1) * degree) break;
        int id = adj[ptr[u]++];
        used[id] = 1;
        if (parity) other.push_back(id);
        else ids[count++] = id;
        parity = !parity;
        u = u < std::size_t(n) ? std::size_t(n) + es[id].r : es[id].l;
      }
      assert(u == s && !parity);
    }
    assert(count == ids.size() / 2 && other.size() == count);
    ids.resize(count);
    return other;
  }

  void assign(const std::vector<int> &ids, int color) {
    for (int id : ids)
      if (es[id].id1 != -1) answer[es[id].id1] = color;
  }

  void solve(std::vector<int> ids, int degree, int offset = 0) {
    assert(degree > 0 && ids.size() == std::size_t(n) * degree);
    if (degree == 1) {
      assign(ids, offset);
    } else if (degree % 2 == 0) {
      auto other = split_even_regular(ids, degree);
      solve(std::move(ids), degree / 2, offset);
      solve(std::move(other), degree / 2, offset + degree / 2);
    } else {
      auto matching = perfect_matching(n, es, ids);
      assign(matching, offset);
      std::size_t count = 0;
      for (int id : ids)
        if (matching[es[id].l] != id) ids[count++] = id;
      ids.resize(count);
      solve(std::move(ids), degree - 1, offset + 1);
    }
  }
};

} // namespace bipartite_edge_coloring_internal

/// @brief Color a bipartite multigraph optimally using colors [0, Delta).
/// Returns colors in input-edge order; duplicate edges are distinct. The input
/// is unchanged, but the optimal coloring is not canonical. Empty input gives
/// an empty result. Original and padded edge counts must fit in int.
inline std::vector<int>
bipartite_edge_coloring(int nl, int nr,
                        const std::vector<std::pair<int, int>> &input) {
  using namespace bipartite_edge_coloring_internal;
  assert(nl >= 0 && nr >= 0);
  assert(input.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (input.empty()) return {};
  std::vector<int> dl(nl), dr(nr);
  int delta = 0;
  for (auto [l, r] : input) {
    assert(0 <= l && l < nl && 0 <= r && r < nr);
    delta = std::max(delta, ++dl[l]);
    delta = std::max(delta, ++dr[r]);
  }
  std::vector<int> answer(input.size());
  if (delta == 1) return answer;
  if (std::size_t(delta) == input.size()) {
    std::iota(answer.begin(), answer.end(), 0);
    return answer;
  }

  auto [li, nl1] = contract_vertices(dl, delta);
  auto [ri, nr1] = contract_vertices(dr, delta);
  int n = std::max(nl1, nr1);
  std::size_t padded = std::size_t(n) * delta;
  assert(padded <= 2 * input.size());
  assert(padded <= std::size_t(std::numeric_limits<int>::max()));
  std::vector<int> left(n), right(n);
  std::vector<edge_record> es;
  es.reserve(padded);
  for (int id = 0; id < int(input.size()); ++id) {
    int l = li[input[id].first], r = ri[input[id].second];
    es.push_back({l, r, id});
    ++left[l];
    ++right[r];
  }
  int l = 0, r = 0;
  while (l < n && r < n) {
    while (l < n && left[l] == delta) ++l;
    while (r < n && right[r] == delta) ++r;
    if (l == n || r == n) break;
    int count = std::min(delta - left[l], delta - right[r]);
    for (int k = 0; k < count; ++k) es.push_back({l, r, -1});
    left[l] += count;
    right[r] += count;
  }
  assert(es.size() == padded);
  std::vector<int> ids(es.size());
  std::iota(ids.begin(), ids.end(), 0);
  coloring_context work(n, es, answer);
  work.solve(std::move(ids), delta);
  return answer;
}

} // namespace noya

 // NOYA_BIPARTITE_EDGE_COLORING_HPP