Skip to content

chordal_graph.hpp

SECTIONGraph INCLUDEnoya/graph/chordal_graph.hpp

用线性时间判定弦图并返回完美消除序或无弦环;忽略自环、归一化重边,证据不唯一。采用连续邻接表、权值桶和分组邻接检查。

Complexity: Time: \(O(V + E)\). Space: \(O(V + E)\).

AC 记录:chordal_graph_recognition。

跳到代码 · GitHub ↗

Implementation

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

/// @complexity Time: O(V + E). Space: O(V + E).

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

namespace noya {

/// @brief Chordality certificate: a perfect-elimination ordering, or an
/// induced cycle of length at least four when the graph is not chordal.
struct chordal_graph_result {
  bool ok = false;
  std::vector<int> ord;
  std::vector<int> cyc;
};

/// @brief Recognize an undirected graph, ignoring loops and duplicate edges.
/// ord is a vertex permutation, and is a PEO exactly when ok is true. Otherwise
/// cyc lists distinct vertices of an induced cycle, without repeating its start.
/// Certificates are noncanonical and may change with edge order. Input is unchanged.
inline chordal_graph_result
chordal_graph(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  chordal_graph_result res;
  res.ord.resize(n);
  if (n == 0) {
    assert(es.empty());
    res.ok = true;
    return res;
  }
  std::vector<std::size_t> off(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    if (u == v) continue;
    ++off[u + 1];
    ++off[v + 1];
  }
  for (int u = 0; u < n; ++u) off[u + 1] += off[u];
  std::vector<int> to(off.back());
  {
    auto cursor = off;
    for (auto [u, v] : es) {
      if (u == v) continue;
      to[cursor[u]++] = v;
      to[cursor[v]++] = u;
    }
  }
  {
    // Compact duplicate neighbors in place, retaining each row's input order.
    std::vector<int> seen(n, -1);
    std::size_t write = 0;
    for (int u = 0; u < n; ++u) {
      auto begin = off[u], end = off[u + 1];
      off[u] = write;
      for (auto i = begin; i < end; ++i) {
        int v = to[i];
        if (seen[v] == u) continue;
        seen[v] = u;
        to[write++] = v;
      }
    }
    off[n] = write;
    to.resize(write);
  }
  auto adj = [&](int u) {
    return std::span<const int>(to).subspan(off[u], off[u + 1] - off[u]);
  };
  std::vector<int> pos(n);
  {
    // Intrusive buckets give constant-time removal and weight increments.
    // A selected vertex has weight -1; distinct-neighbor weights are < n.
    std::vector<int> weight(n), head(n, -1), next(n), prev(n);
    for (int u = 0; u < n; ++u) {
      next[u] = u - 1;
      prev[u] = u + 1 < n ? u + 1 : -1;
    }
    head[0] = n - 1;
    int maximum = 0;
    auto erase = [&](int u) {
      if (prev[u] == -1) head[weight[u]] = next[u];
      else next[prev[u]] = next[u];
      if (next[u] != -1) prev[next[u]] = prev[u];
    };
    for (int i = n - 1; i >= 0; --i) {
      while (head[maximum] == -1) --maximum;
      int u = head[maximum];
      erase(u);
      res.ord[i] = u;
      pos[u] = i;
      weight[u] = -1;
      for (int v : adj(u)) {
        if (weight[v] == -1) continue;
        erase(v);
        ++weight[v];
        prev[v] = -1;
        next[v] = head[weight[v]];
        if (next[v] != -1) prev[next[v]] = v;
        head[weight[v]] = v;
        maximum = std::max(maximum, weight[v]);
      }
    }
  }

  int bad = -1, first = -1, second = -1;
  {
    // Group by the earliest later neighbor, then mark its adjacency once for
    // all its children. This checks all PEO conditions in total linear time.
    std::vector<int> head(n, -1), next(n, -1), mark(n, -1);
    for (int u = 0; u < n; ++u) {
      int p = -1;
      for (int v : adj(u))
        if (pos[v] > pos[u] && (p == -1 || pos[v] < pos[p])) p = v;
      if (p != -1) {
        next[u] = head[p];
        head[p] = u;
      }
    }
    for (int p = 0; p < n; ++p) {
      if (head[p] == -1) continue;
      for (int v : adj(p)) mark[v] = p;
      for (int u = head[p]; u != -1; u = next[u]) {
        if (bad != -1 && pos[u] >= pos[bad]) continue;
        for (int v : adj(u)) {
          if (pos[v] > pos[p] && mark[v] != p) {
            bad = u;
            first = p;
            second = v;
            break;
          }
        }
      }
    }
  }
  if (bad == -1) {
    res.ok = true;
    return res;
  }

  // For the first failing reverse-MCS vertex, connect the two nonadjacent
  // later neighbors while excluding its other neighbors. A shortest path
  // has no chords, and adding bad closes an induced cycle.
  std::vector<int> pre(n, -1), queue{second};
  queue.reserve(n);
  pre[bad] = -2;
  for (int v : adj(bad)) pre[v] = -2;
  pre[first] = -1;
  pre[second] = second;
  for (std::size_t h = 0; h < queue.size() && pre[first] == -1; ++h) {
    for (int v : adj(queue[h])) {
      if (pre[v] != -1 || pos[v] <= pos[bad]) continue;
      pre[v] = queue[h];
      queue.push_back(v);
    }
  }
  assert(pre[first] != -1);
  res.cyc.push_back(bad);
  for (int u = first; u != second; u = pre[u]) res.cyc.push_back(u);
  res.cyc.push_back(second);
  return res;
}

} // namespace noya

 // NOYA_CHORDAL_GRAPH_HPP
#ifndef NOYA_CHORDAL_GRAPH_HPP
#define NOYA_CHORDAL_GRAPH_HPP 1

/// @complexity Time: O(V + E). Space: O(V + E).

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

namespace noya {

/// @brief Chordality certificate: a perfect-elimination ordering, or an
/// induced cycle of length at least four when the graph is not chordal.
struct chordal_graph_result {
  bool ok = false;
  std::vector<int> ord;
  std::vector<int> cyc;
};

/// @brief Recognize an undirected graph, ignoring loops and duplicate edges.
/// ord is a vertex permutation, and is a PEO exactly when ok is true. Otherwise
/// cyc lists distinct vertices of an induced cycle, without repeating its start.
/// Certificates are noncanonical and may change with edge order. Input is unchanged.
inline chordal_graph_result
chordal_graph(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  chordal_graph_result res;
  res.ord.resize(n);
  if (n == 0) {
    assert(es.empty());
    res.ok = true;
    return res;
  }
  std::vector<std::size_t> off(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    if (u == v) continue;
    ++off[u + 1];
    ++off[v + 1];
  }
  for (int u = 0; u < n; ++u) off[u + 1] += off[u];
  std::vector<int> to(off.back());
  {
    auto cursor = off;
    for (auto [u, v] : es) {
      if (u == v) continue;
      to[cursor[u]++] = v;
      to[cursor[v]++] = u;
    }
  }
  {
    // Compact duplicate neighbors in place, retaining each row's input order.
    std::vector<int> seen(n, -1);
    std::size_t write = 0;
    for (int u = 0; u < n; ++u) {
      auto begin = off[u], end = off[u + 1];
      off[u] = write;
      for (auto i = begin; i < end; ++i) {
        int v = to[i];
        if (seen[v] == u) continue;
        seen[v] = u;
        to[write++] = v;
      }
    }
    off[n] = write;
    to.resize(write);
  }
  auto adj = [&](int u) {
    return std::span<const int>(to).subspan(off[u], off[u + 1] - off[u]);
  };
  std::vector<int> pos(n);
  {
    // Intrusive buckets give constant-time removal and weight increments.
    // A selected vertex has weight -1; distinct-neighbor weights are < n.
    std::vector<int> weight(n), head(n, -1), next(n), prev(n);
    for (int u = 0; u < n; ++u) {
      next[u] = u - 1;
      prev[u] = u + 1 < n ? u + 1 : -1;
    }
    head[0] = n - 1;
    int maximum = 0;
    auto erase = [&](int u) {
      if (prev[u] == -1) head[weight[u]] = next[u];
      else next[prev[u]] = next[u];
      if (next[u] != -1) prev[next[u]] = prev[u];
    };
    for (int i = n - 1; i >= 0; --i) {
      while (head[maximum] == -1) --maximum;
      int u = head[maximum];
      erase(u);
      res.ord[i] = u;
      pos[u] = i;
      weight[u] = -1;
      for (int v : adj(u)) {
        if (weight[v] == -1) continue;
        erase(v);
        ++weight[v];
        prev[v] = -1;
        next[v] = head[weight[v]];
        if (next[v] != -1) prev[next[v]] = v;
        head[weight[v]] = v;
        maximum = std::max(maximum, weight[v]);
      }
    }
  }

  int bad = -1, first = -1, second = -1;
  {
    // Group by the earliest later neighbor, then mark its adjacency once for
    // all its children. This checks all PEO conditions in total linear time.
    std::vector<int> head(n, -1), next(n, -1), mark(n, -1);
    for (int u = 0; u < n; ++u) {
      int p = -1;
      for (int v : adj(u))
        if (pos[v] > pos[u] && (p == -1 || pos[v] < pos[p])) p = v;
      if (p != -1) {
        next[u] = head[p];
        head[p] = u;
      }
    }
    for (int p = 0; p < n; ++p) {
      if (head[p] == -1) continue;
      for (int v : adj(p)) mark[v] = p;
      for (int u = head[p]; u != -1; u = next[u]) {
        if (bad != -1 && pos[u] >= pos[bad]) continue;
        for (int v : adj(u)) {
          if (pos[v] > pos[p] && mark[v] != p) {
            bad = u;
            first = p;
            second = v;
            break;
          }
        }
      }
    }
  }
  if (bad == -1) {
    res.ok = true;
    return res;
  }

  // For the first failing reverse-MCS vertex, connect the two nonadjacent
  // later neighbors while excluding its other neighbors. A shortest path
  // has no chords, and adding bad closes an induced cycle.
  std::vector<int> pre(n, -1), queue{second};
  queue.reserve(n);
  pre[bad] = -2;
  for (int v : adj(bad)) pre[v] = -2;
  pre[first] = -1;
  pre[second] = second;
  for (std::size_t h = 0; h < queue.size() && pre[first] == -1; ++h) {
    for (int v : adj(queue[h])) {
      if (pre[v] != -1 || pos[v] <= pos[bad]) continue;
      pre[v] = queue[h];
      queue.push_back(v);
    }
  }
  assert(pre[first] != -1);
  res.cyc.push_back(bad);
  for (int u = first; u != second; u = pre[u]) res.cyc.push_back(u);
  res.cyc.push_back(second);
  return res;
}

} // namespace noya

#endif // NOYA_CHORDAL_GRAPH_HPP
/// @complexity Time: O(V + E). Space: O(V + E).

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

namespace noya {

/// @brief Chordality certificate: a perfect-elimination ordering, or an
/// induced cycle of length at least four when the graph is not chordal.
struct chordal_graph_result {
  bool ok = false;
  std::vector<int> ord;
  std::vector<int> cyc;
};

/// @brief Recognize an undirected graph, ignoring loops and duplicate edges.
/// ord is a vertex permutation, and is a PEO exactly when ok is true. Otherwise
/// cyc lists distinct vertices of an induced cycle, without repeating its start.
/// Certificates are noncanonical and may change with edge order. Input is unchanged.
inline chordal_graph_result
chordal_graph(int n, const std::vector<std::pair<int, int>> &es) {
  assert(n >= 0);
  chordal_graph_result res;
  res.ord.resize(n);
  if (n == 0) {
    assert(es.empty());
    res.ok = true;
    return res;
  }
  std::vector<std::size_t> off(std::size_t(n) + 1);
  for (auto [u, v] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n);
    if (u == v) continue;
    ++off[u + 1];
    ++off[v + 1];
  }
  for (int u = 0; u < n; ++u) off[u + 1] += off[u];
  std::vector<int> to(off.back());
  {
    auto cursor = off;
    for (auto [u, v] : es) {
      if (u == v) continue;
      to[cursor[u]++] = v;
      to[cursor[v]++] = u;
    }
  }
  {
    // Compact duplicate neighbors in place, retaining each row's input order.
    std::vector<int> seen(n, -1);
    std::size_t write = 0;
    for (int u = 0; u < n; ++u) {
      auto begin = off[u], end = off[u + 1];
      off[u] = write;
      for (auto i = begin; i < end; ++i) {
        int v = to[i];
        if (seen[v] == u) continue;
        seen[v] = u;
        to[write++] = v;
      }
    }
    off[n] = write;
    to.resize(write);
  }
  auto adj = [&](int u) {
    return std::span<const int>(to).subspan(off[u], off[u + 1] - off[u]);
  };
  std::vector<int> pos(n);
  {
    // Intrusive buckets give constant-time removal and weight increments.
    // A selected vertex has weight -1; distinct-neighbor weights are < n.
    std::vector<int> weight(n), head(n, -1), next(n), prev(n);
    for (int u = 0; u < n; ++u) {
      next[u] = u - 1;
      prev[u] = u + 1 < n ? u + 1 : -1;
    }
    head[0] = n - 1;
    int maximum = 0;
    auto erase = [&](int u) {
      if (prev[u] == -1) head[weight[u]] = next[u];
      else next[prev[u]] = next[u];
      if (next[u] != -1) prev[next[u]] = prev[u];
    };
    for (int i = n - 1; i >= 0; --i) {
      while (head[maximum] == -1) --maximum;
      int u = head[maximum];
      erase(u);
      res.ord[i] = u;
      pos[u] = i;
      weight[u] = -1;
      for (int v : adj(u)) {
        if (weight[v] == -1) continue;
        erase(v);
        ++weight[v];
        prev[v] = -1;
        next[v] = head[weight[v]];
        if (next[v] != -1) prev[next[v]] = v;
        head[weight[v]] = v;
        maximum = std::max(maximum, weight[v]);
      }
    }
  }

  int bad = -1, first = -1, second = -1;
  {
    // Group by the earliest later neighbor, then mark its adjacency once for
    // all its children. This checks all PEO conditions in total linear time.
    std::vector<int> head(n, -1), next(n, -1), mark(n, -1);
    for (int u = 0; u < n; ++u) {
      int p = -1;
      for (int v : adj(u))
        if (pos[v] > pos[u] && (p == -1 || pos[v] < pos[p])) p = v;
      if (p != -1) {
        next[u] = head[p];
        head[p] = u;
      }
    }
    for (int p = 0; p < n; ++p) {
      if (head[p] == -1) continue;
      for (int v : adj(p)) mark[v] = p;
      for (int u = head[p]; u != -1; u = next[u]) {
        if (bad != -1 && pos[u] >= pos[bad]) continue;
        for (int v : adj(u)) {
          if (pos[v] > pos[p] && mark[v] != p) {
            bad = u;
            first = p;
            second = v;
            break;
          }
        }
      }
    }
  }
  if (bad == -1) {
    res.ok = true;
    return res;
  }

  // For the first failing reverse-MCS vertex, connect the two nonadjacent
  // later neighbors while excluding its other neighbors. A shortest path
  // has no chords, and adding bad closes an induced cycle.
  std::vector<int> pre(n, -1), queue{second};
  queue.reserve(n);
  pre[bad] = -2;
  for (int v : adj(bad)) pre[v] = -2;
  pre[first] = -1;
  pre[second] = second;
  for (std::size_t h = 0; h < queue.size() && pre[first] == -1; ++h) {
    for (int v : adj(queue[h])) {
      if (pre[v] != -1 || pos[v] <= pos[bad]) continue;
      pre[v] = queue[h];
      queue.push_back(v);
    }
  }
  assert(pre[first] != -1);
  res.cyc.push_back(bad);
  for (int u = first; u != second; u = pre[u]) res.cyc.push_back(u);
  res.cyc.push_back(second);
  return res;
}

} // namespace noya

 // NOYA_CHORDAL_GRAPH_HPP