Skip to content

cycle.hpp

SECTIONGraph INCLUDEnoya/graph/cycle.hpp

判断有向图或无向图是否有环,也能返回环上的点或边;拓扑排序返回字典序最小的合法顶点序列。支持自环和重边。

Complexity: Time: \(O(V + E)\) cycle finding and directed detection; \(O(V + E \alpha (V))\) undirected detection; \(O(E + V \log (V + 1))\) lexicographic topological sort. Space: \(O(V + E)\).

AC 记录:cycle_detection, cycle_detection_undirected。

跳到代码 · GitHub ↗

Implementation

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

/// @complexity Time: O(V + E) cycle finding and directed detection;
/// O(V + E alpha(V)) undirected detection; O(E + V log(V+1)) lexicographic
/// topological sort. Space: O(V + E).

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <functional>
#include <limits>
#include <optional>
#include <queue>
#include <utility>
#include <vector>

namespace noya {

struct undirected_cycle {
  std::vector<int> vs;
  std::vector<int> es;
};

namespace cycle_detail {

// Stable CSR retains input order; the path records vertices and entering edges.
template <bool directed>
inline std::optional<undirected_cycle>
find(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (es.empty()) return std::nullopt;
  std::vector<std::size_t> start(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    ++start[u];
    if constexpr (!directed) ++start[v];
  }
  for (int u = 0; u < n; u++) start[u + 1] += start[u];
  std::vector<int> adj(start.back());
  // Reverse insertion preserves input edge order in every CSR row.
  for (int e = int(es.size()); e-- > 0;) {
    auto [u, v] = es[e];
    adj[--start[u]] = e;
    if constexpr (!directed) adj[--start[v]] = e;
  }
  std::vector<std::pair<int, int>> path;
  std::vector<int> pos(n, -1); // -1 unseen, -2 finished, otherwise on the path.
  undirected_cycle res;
  auto dfs = [&](auto &self, int u, int incoming) -> bool {
    pos[u] = int(path.size());
    path.emplace_back(u, incoming);
    for (auto i = start[u]; i < start[u + 1]; ++i) {
      int e = adj[i];
      if constexpr (!directed) if (e == incoming) continue;
      int v = directed ? es[e].second : es[e].first ^ es[e].second ^ u;
      if (pos[v] == -1) {
        if (self(self, v, e)) return true;
      } else if (pos[v] >= 0) {
        if constexpr (!directed)
          for (int j = pos[v]; j < int(path.size()); ++j)
            res.vs.push_back(path[j].first);
        for (int j = pos[v] + 1; j < int(path.size()); ++j)
          res.es.push_back(path[j].second);
        res.es.push_back(e);
        return true;
      }
    }
    path.pop_back();
    pos[u] = -2;
    return false;
  };
  for (int u = 0; u < n; ++u)
    if (pos[u] == -1 && dfs(dfs, u, -1)) return res;
  return std::nullopt;
}

} // namespace cycle_detail

/// @brief Return the edge IDs of one directed cycle in traversal order.
/// Loops and parallel edges are allowed. Search roots by ID and edges in input
/// order; return nullopt if acyclic. Require at most INT_MAX input edges.
inline std::optional<std::vector<int>>
find_directed_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  auto res = cycle_detail::find<true>(n, es);
  if (!res) return std::nullopt;
  return std::move(res->es);
}

/// @brief Return one simple undirected cycle as aligned vertex and edge IDs.
/// A loop is a one-edge cycle; two parallel edges form a two-edge cycle.
/// Preserve the same DFS order as find_directed_cycle, skipping the entering
/// edge ID rather than the parent vertex. Require at most INT_MAX input edges.
inline std::optional<undirected_cycle>
find_undirected_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  return cycle_detail::find<false>(n, es);
}

/// @brief Return the lexicographically smallest topological order, or an empty
/// vector if the directed graph contains a cycle. Vertex count and indegrees
/// must fit int; endpoints must be valid. Input is unchanged.
inline std::vector<int>
topological_sort(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int N = int(g.size());
  std::vector<int> deg(N);
  for (const auto &gi : g) {
    for (int v : gi) {
      assert(0 <= v && v < N);
      assert(deg[v] < std::numeric_limits<int>::max());
      deg[v] += 1;
    }
  }
  std::priority_queue<int, std::vector<int>, std::greater<>> que;
  for (int i = 0; i < N; i++) {
    if (deg[i] == 0) {
      que.push(i);
    }
  }

  std::vector<int> ord;
  while (!que.empty()) {
    int u = que.top();
    que.pop();
    ord.push_back(u);
    for (auto v : g[u]) {
      deg[v] -= 1;
      if (!deg[v]) {
        que.push(v);
      }
    }
  }
  if (int(ord.size()) != N) {
    return {};
  }
  return ord;
}

/// @brief Linear-time directed cycle check by FIFO indegree elimination.
/// Same input preconditions as topological_sort; no ordering is requested.
inline bool cycle_detection_directed(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int n = int(g.size());
  std::vector<int> deg(n);
  for (const auto &row : g) {
    for (int v : row) {
      assert(0 <= v && v < n);
      assert(deg[v] < std::numeric_limits<int>::max());
      ++deg[v];
    }
  }
  std::vector<int> q;
  q.reserve(n);
  for (int u = 0; u < n; u++) if (deg[u] == 0) q.push_back(u);
  for (std::size_t head = 0; head < q.size(); head++) {
    for (int v : g[q[head]]) if (--deg[v] == 0) q.push_back(v);
  }
  return q.size() != g.size();
}

/// @brief Detect whether an undirected edge list contains a cycle.
/// Loops and parallel edges count as cycles. IDs must be in [0, INT_MAX);
/// V in the complexity bound is one plus the largest ID (zero for no edges).
inline bool
cycle_detection_undirected(const std::vector<std::pair<int, int>> &es) {
  if (es.empty()) return false;
  int n = 0;
  for (auto [a, b] : es) {
    assert(0 <= a && a < std::numeric_limits<int>::max());
    assert(0 <= b && b < std::numeric_limits<int>::max());
    n = std::max(n, std::max(a, b) + 1);
  }
  std::vector<int> p(n, -1);
  auto leader = [&](int u) {
    int r = u;
    while (p[r] >= 0) r = p[r];
    while (u != r) {
      int v = p[u];
      p[u] = r;
      u = v;
    }
    return r;
  };
  for (auto [a, b] : es) {
    a = leader(a);
    b = leader(b);
    if (a == b) return true;
    if (p[a] > p[b]) std::swap(a, b);
    p[a] += p[b];
    p[b] = a;
  }
  return false;
}
} // namespace noya

 // NOYA_CYCLE_HPP
#ifndef NOYA_CYCLE_HPP
#define NOYA_CYCLE_HPP 1

/// @complexity Time: O(V + E) cycle finding and directed detection;
/// O(V + E alpha(V)) undirected detection; O(E + V log(V+1)) lexicographic
/// topological sort. Space: O(V + E).

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <functional>
#include <limits>
#include <optional>
#include <queue>
#include <utility>
#include <vector>

namespace noya {

struct undirected_cycle {
  std::vector<int> vs;
  std::vector<int> es;
};

namespace cycle_detail {

// Stable CSR retains input order; the path records vertices and entering edges.
template <bool directed>
inline std::optional<undirected_cycle>
find(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (es.empty()) return std::nullopt;
  std::vector<std::size_t> start(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    ++start[u];
    if constexpr (!directed) ++start[v];
  }
  for (int u = 0; u < n; u++) start[u + 1] += start[u];
  std::vector<int> adj(start.back());
  // Reverse insertion preserves input edge order in every CSR row.
  for (int e = int(es.size()); e-- > 0;) {
    auto [u, v] = es[e];
    adj[--start[u]] = e;
    if constexpr (!directed) adj[--start[v]] = e;
  }
  std::vector<std::pair<int, int>> path;
  std::vector<int> pos(n, -1); // -1 unseen, -2 finished, otherwise on the path.
  undirected_cycle res;
  auto dfs = [&](auto &self, int u, int incoming) -> bool {
    pos[u] = int(path.size());
    path.emplace_back(u, incoming);
    for (auto i = start[u]; i < start[u + 1]; ++i) {
      int e = adj[i];
      if constexpr (!directed) if (e == incoming) continue;
      int v = directed ? es[e].second : es[e].first ^ es[e].second ^ u;
      if (pos[v] == -1) {
        if (self(self, v, e)) return true;
      } else if (pos[v] >= 0) {
        if constexpr (!directed)
          for (int j = pos[v]; j < int(path.size()); ++j)
            res.vs.push_back(path[j].first);
        for (int j = pos[v] + 1; j < int(path.size()); ++j)
          res.es.push_back(path[j].second);
        res.es.push_back(e);
        return true;
      }
    }
    path.pop_back();
    pos[u] = -2;
    return false;
  };
  for (int u = 0; u < n; ++u)
    if (pos[u] == -1 && dfs(dfs, u, -1)) return res;
  return std::nullopt;
}

} // namespace cycle_detail

/// @brief Return the edge IDs of one directed cycle in traversal order.
/// Loops and parallel edges are allowed. Search roots by ID and edges in input
/// order; return nullopt if acyclic. Require at most INT_MAX input edges.
inline std::optional<std::vector<int>>
find_directed_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  auto res = cycle_detail::find<true>(n, es);
  if (!res) return std::nullopt;
  return std::move(res->es);
}

/// @brief Return one simple undirected cycle as aligned vertex and edge IDs.
/// A loop is a one-edge cycle; two parallel edges form a two-edge cycle.
/// Preserve the same DFS order as find_directed_cycle, skipping the entering
/// edge ID rather than the parent vertex. Require at most INT_MAX input edges.
inline std::optional<undirected_cycle>
find_undirected_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  return cycle_detail::find<false>(n, es);
}

/// @brief Return the lexicographically smallest topological order, or an empty
/// vector if the directed graph contains a cycle. Vertex count and indegrees
/// must fit int; endpoints must be valid. Input is unchanged.
inline std::vector<int>
topological_sort(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int N = int(g.size());
  std::vector<int> deg(N);
  for (const auto &gi : g) {
    for (int v : gi) {
      assert(0 <= v && v < N);
      assert(deg[v] < std::numeric_limits<int>::max());
      deg[v] += 1;
    }
  }
  std::priority_queue<int, std::vector<int>, std::greater<>> que;
  for (int i = 0; i < N; i++) {
    if (deg[i] == 0) {
      que.push(i);
    }
  }

  std::vector<int> ord;
  while (!que.empty()) {
    int u = que.top();
    que.pop();
    ord.push_back(u);
    for (auto v : g[u]) {
      deg[v] -= 1;
      if (!deg[v]) {
        que.push(v);
      }
    }
  }
  if (int(ord.size()) != N) {
    return {};
  }
  return ord;
}

/// @brief Linear-time directed cycle check by FIFO indegree elimination.
/// Same input preconditions as topological_sort; no ordering is requested.
inline bool cycle_detection_directed(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int n = int(g.size());
  std::vector<int> deg(n);
  for (const auto &row : g) {
    for (int v : row) {
      assert(0 <= v && v < n);
      assert(deg[v] < std::numeric_limits<int>::max());
      ++deg[v];
    }
  }
  std::vector<int> q;
  q.reserve(n);
  for (int u = 0; u < n; u++) if (deg[u] == 0) q.push_back(u);
  for (std::size_t head = 0; head < q.size(); head++) {
    for (int v : g[q[head]]) if (--deg[v] == 0) q.push_back(v);
  }
  return q.size() != g.size();
}

/// @brief Detect whether an undirected edge list contains a cycle.
/// Loops and parallel edges count as cycles. IDs must be in [0, INT_MAX);
/// V in the complexity bound is one plus the largest ID (zero for no edges).
inline bool
cycle_detection_undirected(const std::vector<std::pair<int, int>> &es) {
  if (es.empty()) return false;
  int n = 0;
  for (auto [a, b] : es) {
    assert(0 <= a && a < std::numeric_limits<int>::max());
    assert(0 <= b && b < std::numeric_limits<int>::max());
    n = std::max(n, std::max(a, b) + 1);
  }
  std::vector<int> p(n, -1);
  auto leader = [&](int u) {
    int r = u;
    while (p[r] >= 0) r = p[r];
    while (u != r) {
      int v = p[u];
      p[u] = r;
      u = v;
    }
    return r;
  };
  for (auto [a, b] : es) {
    a = leader(a);
    b = leader(b);
    if (a == b) return true;
    if (p[a] > p[b]) std::swap(a, b);
    p[a] += p[b];
    p[b] = a;
  }
  return false;
}
} // namespace noya

#endif // NOYA_CYCLE_HPP
/// @complexity Time: O(V + E) cycle finding and directed detection;
/// O(V + E alpha(V)) undirected detection; O(E + V log(V+1)) lexicographic
/// topological sort. Space: O(V + E).

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <functional>
#include <limits>
#include <optional>
#include <queue>
#include <utility>
#include <vector>

namespace noya {

struct undirected_cycle {
  std::vector<int> vs;
  std::vector<int> es;
};

namespace cycle_detail {

// Stable CSR retains input order; the path records vertices and entering edges.
template <bool directed>
inline std::optional<undirected_cycle>
find(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  if (es.empty()) return std::nullopt;
  std::vector<std::size_t> start(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    ++start[u];
    if constexpr (!directed) ++start[v];
  }
  for (int u = 0; u < n; u++) start[u + 1] += start[u];
  std::vector<int> adj(start.back());
  // Reverse insertion preserves input edge order in every CSR row.
  for (int e = int(es.size()); e-- > 0;) {
    auto [u, v] = es[e];
    adj[--start[u]] = e;
    if constexpr (!directed) adj[--start[v]] = e;
  }
  std::vector<std::pair<int, int>> path;
  std::vector<int> pos(n, -1); // -1 unseen, -2 finished, otherwise on the path.
  undirected_cycle res;
  auto dfs = [&](auto &self, int u, int incoming) -> bool {
    pos[u] = int(path.size());
    path.emplace_back(u, incoming);
    for (auto i = start[u]; i < start[u + 1]; ++i) {
      int e = adj[i];
      if constexpr (!directed) if (e == incoming) continue;
      int v = directed ? es[e].second : es[e].first ^ es[e].second ^ u;
      if (pos[v] == -1) {
        if (self(self, v, e)) return true;
      } else if (pos[v] >= 0) {
        if constexpr (!directed)
          for (int j = pos[v]; j < int(path.size()); ++j)
            res.vs.push_back(path[j].first);
        for (int j = pos[v] + 1; j < int(path.size()); ++j)
          res.es.push_back(path[j].second);
        res.es.push_back(e);
        return true;
      }
    }
    path.pop_back();
    pos[u] = -2;
    return false;
  };
  for (int u = 0; u < n; ++u)
    if (pos[u] == -1 && dfs(dfs, u, -1)) return res;
  return std::nullopt;
}

} // namespace cycle_detail

/// @brief Return the edge IDs of one directed cycle in traversal order.
/// Loops and parallel edges are allowed. Search roots by ID and edges in input
/// order; return nullopt if acyclic. Require at most INT_MAX input edges.
inline std::optional<std::vector<int>>
find_directed_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  auto res = cycle_detail::find<true>(n, es);
  if (!res) return std::nullopt;
  return std::move(res->es);
}

/// @brief Return one simple undirected cycle as aligned vertex and edge IDs.
/// A loop is a one-edge cycle; two parallel edges form a two-edge cycle.
/// Preserve the same DFS order as find_directed_cycle, skipping the entering
/// edge ID rather than the parent vertex. Require at most INT_MAX input edges.
inline std::optional<undirected_cycle>
find_undirected_cycle(int n, const std::vector<std::pair<int, int>> &es) {
  return cycle_detail::find<false>(n, es);
}

/// @brief Return the lexicographically smallest topological order, or an empty
/// vector if the directed graph contains a cycle. Vertex count and indegrees
/// must fit int; endpoints must be valid. Input is unchanged.
inline std::vector<int>
topological_sort(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int N = int(g.size());
  std::vector<int> deg(N);
  for (const auto &gi : g) {
    for (int v : gi) {
      assert(0 <= v && v < N);
      assert(deg[v] < std::numeric_limits<int>::max());
      deg[v] += 1;
    }
  }
  std::priority_queue<int, std::vector<int>, std::greater<>> que;
  for (int i = 0; i < N; i++) {
    if (deg[i] == 0) {
      que.push(i);
    }
  }

  std::vector<int> ord;
  while (!que.empty()) {
    int u = que.top();
    que.pop();
    ord.push_back(u);
    for (auto v : g[u]) {
      deg[v] -= 1;
      if (!deg[v]) {
        que.push(v);
      }
    }
  }
  if (int(ord.size()) != N) {
    return {};
  }
  return ord;
}

/// @brief Linear-time directed cycle check by FIFO indegree elimination.
/// Same input preconditions as topological_sort; no ordering is requested.
inline bool cycle_detection_directed(const std::vector<std::vector<int>> &g) {
  assert(g.size() <= std::size_t(std::numeric_limits<int>::max()));
  int n = int(g.size());
  std::vector<int> deg(n);
  for (const auto &row : g) {
    for (int v : row) {
      assert(0 <= v && v < n);
      assert(deg[v] < std::numeric_limits<int>::max());
      ++deg[v];
    }
  }
  std::vector<int> q;
  q.reserve(n);
  for (int u = 0; u < n; u++) if (deg[u] == 0) q.push_back(u);
  for (std::size_t head = 0; head < q.size(); head++) {
    for (int v : g[q[head]]) if (--deg[v] == 0) q.push_back(v);
  }
  return q.size() != g.size();
}

/// @brief Detect whether an undirected edge list contains a cycle.
/// Loops and parallel edges count as cycles. IDs must be in [0, INT_MAX);
/// V in the complexity bound is one plus the largest ID (zero for no edges).
inline bool
cycle_detection_undirected(const std::vector<std::pair<int, int>> &es) {
  if (es.empty()) return false;
  int n = 0;
  for (auto [a, b] : es) {
    assert(0 <= a && a < std::numeric_limits<int>::max());
    assert(0 <= b && b < std::numeric_limits<int>::max());
    n = std::max(n, std::max(a, b) + 1);
  }
  std::vector<int> p(n, -1);
  auto leader = [&](int u) {
    int r = u;
    while (p[r] >= 0) r = p[r];
    while (u != r) {
      int v = p[u];
      p[u] = r;
      u = v;
    }
    return r;
  };
  for (auto [a, b] : es) {
    a = leader(a);
    b = leader(b);
    if (a == b) return true;
    if (p[a] > p[b]) std::swap(a, b);
    p[a] += p[b];
    p[b] = a;
  }
  return false;
}
} // namespace noya

 // NOYA_CYCLE_HPP