Skip to content

small_cycle_count.hpp

SECTIONGraph INCLUDEnoya/graph/small_cycle_count.hpp

统计三角形、带顶点权的三角形乘积和及非诱导四环;无权接口忽略自环与重边,加权接口要求简单图。度数定向使计数核心为 O(\(m\)√\(m\)),四环使用顶点计数数组。

Complexity: Time: \(O(n + m \log (m + 1) + M \sqrt{M})\) unweighted counts, \(O(n + m \sqrt{m})\) weighted triangles; \(M\) distinct non-loop edges. Space: \(O(n + m)\). Weight arithmetic is assumed constant-time.

AC 记录:enumerate_triangles。

跳到代码 · GitHub ↗

Implementation

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

/// @complexity Time: O(n+m log(m+1)+M sqrt(M)) unweighted counts,
/// O(n+m sqrt(m)) weighted triangles; M distinct non-loop edges.
/// Space: O(n+m). Weight arithmetic is assumed constant-time.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace small_cycle_count_internal {

inline std::vector<std::pair<int, int>>
simple_edges(int n, std::vector<std::pair<int, int>> es) {
  assert(n >= 0);
  for (auto &[a, b] : es) {
    assert(0 <= a && a < n);
    assert(0 <= b && b < n);
    if (a > b) std::swap(a, b);
  }
  es.erase(std::remove_if(es.begin(), es.end(),
                         [](auto e) { return e.first == e.second; }), es.end());
  if (!std::is_sorted(es.begin(), es.end())) std::sort(es.begin(), es.end());
  es.erase(std::unique(es.begin(), es.end()), es.end());
  return es;
}

struct forward_graph {
  std::vector<std::size_t> head;
  std::vector<int> to;
};

// Simple input only. Preserve the (degree,id) orientation and input-edge order.
inline forward_graph orient(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  std::vector<int> deg(n);
  for (auto [a, b] : es) {
    assert(0 <= a && a < n && 0 <= b && b < n && a != b);
    ++deg[a]; ++deg[b];
  }
  std::vector<std::size_t> head(std::size_t(n) + 1);
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    ++head[a + 1];
  }
  for (int a = 0; a < n; ++a) head[a + 1] += head[a];
  auto next = head;
  std::vector<int> to(es.size());
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    to[next[a]++] = b;
  }
  return {std::move(head), std::move(to)};
}

} // namespace small_cycle_count_internal

/// @brief Count triangles in an undirected graph, ignoring duplicate edges
/// and self-loops. Require n>=0, valid endpoints and a uint64_t result.
/// Normalization costs O(m log(m+1)); the oriented core costs O(n+M sqrt(M)).
inline std::uint64_t triangle_count(int n,
                                  std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 3) return 0;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  std::uint64_t result = 0;
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) result += marked[to[j]];
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Sum w[a]*w[b]*w[c] over triangles of a SIMPLE undirected graph.
/// Weighted input must have no loops or parallel edges, including reversals.
/// Preserve (degree,id) product order and the original accumulation order.
/// Require n>=0, valid endpoints, w.size()==n and representable arithmetic.
/// Inputs are unchanged; unlike the unweighted counts, no normalization occurs.
template <class T>
T weighted_triangle_sum(int n, const std::vector<std::pair<int, int>> &es,
                        const std::vector<T> &w) {
  assert(n >= 0 && int(w.size()) == n);
  T result{};
  if (es.empty()) return result;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) {
        int c = to[j];
        if (marked[c]) result += w[a] * w[b] * w[c];
      }
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Count non-induced four-cycles with four distinct vertices.
/// Ignore loops and duplicate edges; require n>=0, valid endpoints and a
/// uint64_t result. Normalize in O(m log(m+1)), then count in O(n+M sqrt(M)).
inline std::uint64_t four_cycle_count(int n,
                                    std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 4) return 0;
  std::vector<int> deg(n);
  for (auto [a, b] : es) { ++deg[a]; ++deg[b]; }
  auto lower = [&](int a, int b) {
    return std::tie(deg[a], a) < std::tie(deg[b], b);
  };
  std::vector<std::vector<int>> g(n);
  for (int u = 0; u < n; ++u) g[u].reserve(deg[u]);
  for (auto [a, b] : es) { g[a].push_back(b); g[b].push_back(a); }
  std::vector<int> count(n), touched;
  std::uint64_t result = 0;
  for (int u = 0; u < n; ++u) {
    // Count each cycle at its unique maximum (degree,id) vertex u.
    for (int v : g[u]) if (lower(v, u))
      for (int w : g[v]) if (lower(w, u)) {
        if (count[w] == 0) touched.push_back(w);
        result += count[w]++;
      }
    for (int w : touched) count[w] = 0;
    touched.clear();
  }
  return result;
}

} // namespace noya
 // NOYA_SMALL_CYCLE_COUNT_HPP
#ifndef NOYA_SMALL_CYCLE_COUNT_HPP
#define NOYA_SMALL_CYCLE_COUNT_HPP 1

/// @complexity Time: O(n+m log(m+1)+M sqrt(M)) unweighted counts,
/// O(n+m sqrt(m)) weighted triangles; M distinct non-loop edges.
/// Space: O(n+m). Weight arithmetic is assumed constant-time.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace small_cycle_count_internal {

inline std::vector<std::pair<int, int>>
simple_edges(int n, std::vector<std::pair<int, int>> es) {
  assert(n >= 0);
  for (auto &[a, b] : es) {
    assert(0 <= a && a < n);
    assert(0 <= b && b < n);
    if (a > b) std::swap(a, b);
  }
  es.erase(std::remove_if(es.begin(), es.end(),
                         [](auto e) { return e.first == e.second; }), es.end());
  if (!std::is_sorted(es.begin(), es.end())) std::sort(es.begin(), es.end());
  es.erase(std::unique(es.begin(), es.end()), es.end());
  return es;
}

struct forward_graph {
  std::vector<std::size_t> head;
  std::vector<int> to;
};

// Simple input only. Preserve the (degree,id) orientation and input-edge order.
inline forward_graph orient(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  std::vector<int> deg(n);
  for (auto [a, b] : es) {
    assert(0 <= a && a < n && 0 <= b && b < n && a != b);
    ++deg[a]; ++deg[b];
  }
  std::vector<std::size_t> head(std::size_t(n) + 1);
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    ++head[a + 1];
  }
  for (int a = 0; a < n; ++a) head[a + 1] += head[a];
  auto next = head;
  std::vector<int> to(es.size());
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    to[next[a]++] = b;
  }
  return {std::move(head), std::move(to)};
}

} // namespace small_cycle_count_internal

/// @brief Count triangles in an undirected graph, ignoring duplicate edges
/// and self-loops. Require n>=0, valid endpoints and a uint64_t result.
/// Normalization costs O(m log(m+1)); the oriented core costs O(n+M sqrt(M)).
inline std::uint64_t triangle_count(int n,
                                  std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 3) return 0;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  std::uint64_t result = 0;
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) result += marked[to[j]];
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Sum w[a]*w[b]*w[c] over triangles of a SIMPLE undirected graph.
/// Weighted input must have no loops or parallel edges, including reversals.
/// Preserve (degree,id) product order and the original accumulation order.
/// Require n>=0, valid endpoints, w.size()==n and representable arithmetic.
/// Inputs are unchanged; unlike the unweighted counts, no normalization occurs.
template <class T>
T weighted_triangle_sum(int n, const std::vector<std::pair<int, int>> &es,
                        const std::vector<T> &w) {
  assert(n >= 0 && int(w.size()) == n);
  T result{};
  if (es.empty()) return result;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) {
        int c = to[j];
        if (marked[c]) result += w[a] * w[b] * w[c];
      }
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Count non-induced four-cycles with four distinct vertices.
/// Ignore loops and duplicate edges; require n>=0, valid endpoints and a
/// uint64_t result. Normalize in O(m log(m+1)), then count in O(n+M sqrt(M)).
inline std::uint64_t four_cycle_count(int n,
                                    std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 4) return 0;
  std::vector<int> deg(n);
  for (auto [a, b] : es) { ++deg[a]; ++deg[b]; }
  auto lower = [&](int a, int b) {
    return std::tie(deg[a], a) < std::tie(deg[b], b);
  };
  std::vector<std::vector<int>> g(n);
  for (int u = 0; u < n; ++u) g[u].reserve(deg[u]);
  for (auto [a, b] : es) { g[a].push_back(b); g[b].push_back(a); }
  std::vector<int> count(n), touched;
  std::uint64_t result = 0;
  for (int u = 0; u < n; ++u) {
    // Count each cycle at its unique maximum (degree,id) vertex u.
    for (int v : g[u]) if (lower(v, u))
      for (int w : g[v]) if (lower(w, u)) {
        if (count[w] == 0) touched.push_back(w);
        result += count[w]++;
      }
    for (int w : touched) count[w] = 0;
    touched.clear();
  }
  return result;
}

} // namespace noya
#endif // NOYA_SMALL_CYCLE_COUNT_HPP
/// @complexity Time: O(n+m log(m+1)+M sqrt(M)) unweighted counts,
/// O(n+m sqrt(m)) weighted triangles; M distinct non-loop edges.
/// Space: O(n+m). Weight arithmetic is assumed constant-time.

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace small_cycle_count_internal {

inline std::vector<std::pair<int, int>>
simple_edges(int n, std::vector<std::pair<int, int>> es) {
  assert(n >= 0);
  for (auto &[a, b] : es) {
    assert(0 <= a && a < n);
    assert(0 <= b && b < n);
    if (a > b) std::swap(a, b);
  }
  es.erase(std::remove_if(es.begin(), es.end(),
                         [](auto e) { return e.first == e.second; }), es.end());
  if (!std::is_sorted(es.begin(), es.end())) std::sort(es.begin(), es.end());
  es.erase(std::unique(es.begin(), es.end()), es.end());
  return es;
}

struct forward_graph {
  std::vector<std::size_t> head;
  std::vector<int> to;
};

// Simple input only. Preserve the (degree,id) orientation and input-edge order.
inline forward_graph orient(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  std::vector<int> deg(n);
  for (auto [a, b] : es) {
    assert(0 <= a && a < n && 0 <= b && b < n && a != b);
    ++deg[a]; ++deg[b];
  }
  std::vector<std::size_t> head(std::size_t(n) + 1);
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    ++head[a + 1];
  }
  for (int a = 0; a < n; ++a) head[a + 1] += head[a];
  auto next = head;
  std::vector<int> to(es.size());
  for (auto [a, b] : es) {
    if (std::tie(deg[a], a) > std::tie(deg[b], b)) std::swap(a, b);
    to[next[a]++] = b;
  }
  return {std::move(head), std::move(to)};
}

} // namespace small_cycle_count_internal

/// @brief Count triangles in an undirected graph, ignoring duplicate edges
/// and self-loops. Require n>=0, valid endpoints and a uint64_t result.
/// Normalization costs O(m log(m+1)); the oriented core costs O(n+M sqrt(M)).
inline std::uint64_t triangle_count(int n,
                                  std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 3) return 0;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  std::uint64_t result = 0;
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) result += marked[to[j]];
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Sum w[a]*w[b]*w[c] over triangles of a SIMPLE undirected graph.
/// Weighted input must have no loops or parallel edges, including reversals.
/// Preserve (degree,id) product order and the original accumulation order.
/// Require n>=0, valid endpoints, w.size()==n and representable arithmetic.
/// Inputs are unchanged; unlike the unweighted counts, no normalization occurs.
template <class T>
T weighted_triangle_sum(int n, const std::vector<std::pair<int, int>> &es,
                        const std::vector<T> &w) {
  assert(n >= 0 && int(w.size()) == n);
  T result{};
  if (es.empty()) return result;
  auto [head, to] = small_cycle_count_internal::orient(n, es);
  std::vector<unsigned char> marked(n);
  for (int a = 0; a < n; ++a) {
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = true;
    for (auto i = head[a]; i < head[a + 1]; ++i) {
      int b = to[i];
      for (auto j = head[b]; j < head[b + 1]; ++j) {
        int c = to[j];
        if (marked[c]) result += w[a] * w[b] * w[c];
      }
    }
    for (auto i = head[a]; i < head[a + 1]; ++i) marked[to[i]] = false;
  }
  return result;
}

/// @brief Count non-induced four-cycles with four distinct vertices.
/// Ignore loops and duplicate edges; require n>=0, valid endpoints and a
/// uint64_t result. Normalize in O(m log(m+1)), then count in O(n+M sqrt(M)).
inline std::uint64_t four_cycle_count(int n,
                                    std::vector<std::pair<int, int>> es) {
  es = small_cycle_count_internal::simple_edges(n, std::move(es));
  if (es.size() < 4) return 0;
  std::vector<int> deg(n);
  for (auto [a, b] : es) { ++deg[a]; ++deg[b]; }
  auto lower = [&](int a, int b) {
    return std::tie(deg[a], a) < std::tie(deg[b], b);
  };
  std::vector<std::vector<int>> g(n);
  for (int u = 0; u < n; ++u) g[u].reserve(deg[u]);
  for (auto [a, b] : es) { g[a].push_back(b); g[b].push_back(a); }
  std::vector<int> count(n), touched;
  std::uint64_t result = 0;
  for (int u = 0; u < n; ++u) {
    // Count each cycle at its unique maximum (degree,id) vertex u.
    for (int v : g[u]) if (lower(v, u))
      for (int w : g[v]) if (lower(w, u)) {
        if (count[w] == 0) touched.push_back(w);
        result += count[w]++;
      }
    for (int w : touched) count[w] = 0;
    touched.clear();
  }
  return result;
}

} // namespace noya
 // NOYA_SMALL_CYCLE_COUNT_HPP