use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Edge<E: Clone + Debug + Eq + Hash + Ord> {
from: usize,
to: usize,
directed: bool,
data: E,
}
#[derive(Debug, Clone)]
pub struct Graph<N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
nodes: Vec<(N, H)>,
edges: Vec<Edge<E>>,
adj_out: Vec<Vec<(usize, usize)>>,
adj_in: Vec<Vec<(usize, usize)>>,
}
#[derive(Debug, Clone)]
pub struct CanonicalForm<N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
pub vertex_map: Vec<usize>,
pub orbits: Vec<Vec<usize>>,
pub automorphism_group_size: u64,
pub graph: Graph<N, H, E>,
}
impl<N, H, E> Graph<N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
adj_out: Vec::new(),
adj_in: Vec::new(),
}
}
pub fn add_node(&mut self, data: N, hidden: H) -> usize {
let idx = self.nodes.len();
self.nodes.push((data, hidden));
self.adj_out.push(Vec::new());
self.adj_in.push(Vec::new());
idx
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn node_data(&self, v: usize) -> &N {
&self.nodes[v].0
}
pub fn node_hidden(&self, v: usize) -> &H {
&self.nodes[v].1
}
pub fn add_directed_edge(&mut self, from: usize, to: usize, data: E) {
let idx = self.edges.len();
self.edges.push(Edge {
from,
to,
directed: true,
data,
});
self.adj_out[from].push((idx, to));
self.adj_in[to].push((idx, from));
}
pub fn add_undirected_edge(&mut self, u: usize, v: usize, data: E) {
let idx = self.edges.len();
self.edges.push(Edge {
from: u,
to: v,
directed: false,
data,
});
self.adj_out[u].push((idx, v));
self.adj_out[v].push((idx, u));
}
pub fn edges_of(&self, v: usize) -> EdgeIter<'_, N, H, E> {
EdgeIter {
graph: self,
v,
out_pos: 0,
in_pos: 0,
}
}
#[allow(clippy::needless_range_loop)]
pub fn canonize(&self) -> CanonicalForm<N, H, E> {
let n = self.nodes.len();
if n == 0 {
return CanonicalForm {
vertex_map: Vec::new(),
orbits: Vec::new(),
automorphism_group_size: 1,
graph: self.clone(),
};
}
let mut initial = Partition::from_graph(self);
initial.refine(self);
let mut stack: Vec<SearchFrame> = Vec::new();
let root_inv = Invariant::from_partition(&initial, n);
stack.push(SearchFrame {
partition: initial,
invariant: root_inv,
is_leftmost: true,
});
let mut best_labeling: Option<Vec<usize>> = None;
let mut best_cert: Option<Vec<(usize, usize, E, u8)>> = None;
let mut orbit_generators: Vec<Vec<usize>> = Vec::new();
let mut leaf_seen: HashMap<u64, Vec<usize>> = HashMap::new();
while let Some(frame) = stack.pop() {
if frame.partition.is_discrete() {
let labelling: Vec<usize> = frame.partition.labeling();
let cert = self.certificate(&labelling);
match best_cert.as_ref() {
None => {
best_labeling = Some(labelling.clone());
best_cert = Some(cert);
leaf_seen.clear();
leaf_seen.insert(hash_slice(best_cert.as_ref().unwrap()), labelling);
}
Some(prev) if cert > *prev => {
best_labeling = Some(labelling.clone());
best_cert = Some(cert);
orbit_generators.clear();
leaf_seen.clear();
leaf_seen.insert(hash_slice(best_cert.as_ref().unwrap()), labelling);
}
Some(prev) if cert == *prev => {
if let Some(ref best_lab) = best_labeling {
let autom = compose_permutations(&labelling, best_lab);
orbit_generators.push(autom);
}
}
_ => {}
}
continue;
}
let cell_idx = match frame.partition.smallest_nontrivial_cell() {
Some(ci) => ci,
None => continue,
};
let cell: Vec<usize> = frame.partition.cells[cell_idx].clone();
let cell_len = cell.len();
let mut children: Vec<SearchFrame> = Vec::new();
let mut processed_siblings: Vec<usize> = Vec::new();
for pos in 0..cell_len {
let v = cell[pos];
if orbit_prune(
v,
&processed_siblings,
&orbit_generators,
cell_idx,
&frame.partition,
) {
continue;
}
let mut child_part = frame.partition.clone();
child_part.individualize(cell_idx, pos);
child_part.refine(self);
let child_inv = frame.invariant.extend(child_part.cell_lengths());
if let Some(ref _best_cert) = best_cert
&& best_labeling.is_some()
&& !frame.is_leftmost
{
}
let is_left = frame.is_leftmost && pos == 0;
children.push(SearchFrame {
partition: child_part,
invariant: child_inv,
is_leftmost: is_left,
});
processed_siblings.push(v);
}
for child in children.into_iter().rev() {
stack.push(child);
}
}
let autom_count = if orbit_generators.is_empty() {
1
} else {
group_size(&orbit_generators, n)
};
let labeling = best_labeling.unwrap_or_else(|| (0..n).collect());
let orbits = compute_orbits(&orbit_generators, n);
let canon_graph = self.relabeled(&labeling);
CanonicalForm {
vertex_map: labeling,
orbits,
automorphism_group_size: autom_count,
graph: canon_graph,
}
}
fn certificate(&self, labeling: &[usize]) -> Vec<(usize, usize, E, u8)> {
let n = labeling.len();
let mut pos_of = vec![0usize; n];
for (pos, &v) in labeling.iter().enumerate() {
pos_of[v] = pos;
}
let mut cert: Vec<(usize, usize, E, u8)> = Vec::new();
for (v, out_list) in self.adj_out.iter().enumerate() {
let i = pos_of[v];
for &(edge_idx, other) in out_list {
let edge = &self.edges[edge_idx];
let j = pos_of[other];
let dir = if edge.directed { 0u8 } else { 2u8 };
cert.push((i, j, edge.data.clone(), dir));
}
}
for (v, in_list) in self.adj_in.iter().enumerate() {
let j = pos_of[v]; for &(edge_idx, other) in in_list {
let edge = &self.edges[edge_idx];
let i = pos_of[other]; cert.push((i, j, edge.data.clone(), 1u8));
}
}
cert.sort();
cert
}
fn relabeled(&self, labeling: &[usize]) -> Graph<N, H, E> {
let n = labeling.len();
let mut new_nodes = vec![(self.nodes[0].0.clone(), self.nodes[0].1.clone()); n];
for (new_idx, &old_idx) in labeling.iter().enumerate() {
new_nodes[new_idx] = self.nodes[old_idx].clone();
}
let mut new_graph = Graph {
nodes: new_nodes,
edges: Vec::new(),
adj_out: vec![Vec::new(); n],
adj_in: vec![Vec::new(); n],
};
let mut new_of = vec![0usize; self.nodes.len()];
for (new_idx, &old_idx) in labeling.iter().enumerate() {
new_of[old_idx] = new_idx;
}
for edge in &self.edges {
let new_from = new_of[edge.from];
let new_to = new_of[edge.to];
if edge.directed {
new_graph.add_directed_edge(new_from, new_to, edge.data.clone());
} else {
new_graph.add_undirected_edge(new_from, new_to, edge.data.clone());
}
}
new_graph
}
}
impl<N, H, E> Default for Graph<N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
fn default() -> Self {
Self::new()
}
}
pub struct EdgeIter<'a, N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
graph: &'a Graph<N, H, E>,
v: usize,
out_pos: usize,
in_pos: usize,
}
#[derive(Debug, Clone)]
pub struct EdgeView<E: Clone> {
pub data: E,
pub neighbour: usize,
pub is_directed: bool,
pub is_outgoing: bool,
}
impl<'a, N, H, E> Iterator for EdgeIter<'a, N, H, E>
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
type Item = EdgeView<E>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.out_pos < self.graph.adj_out[self.v].len() {
let (ei, nb) = self.graph.adj_out[self.v][self.out_pos];
self.out_pos += 1;
let edge = &self.graph.edges[ei];
if edge.directed && edge.to == self.v {
continue;
}
return Some(EdgeView {
data: edge.data.clone(),
neighbour: nb,
is_directed: edge.directed,
is_outgoing: true,
});
}
if self.in_pos < self.graph.adj_in[self.v].len() {
let (ei, nb) = self.graph.adj_in[self.v][self.in_pos];
self.in_pos += 1;
return Some(EdgeView {
data: self.graph.edges[ei].data.clone(),
neighbour: nb,
is_directed: true,
is_outgoing: false,
});
}
return None;
}
}
}
fn hash_slice<E: Hash>(slice: &[E]) -> u64 {
use std::hash::Hasher;
let mut h = std::collections::hash_map::DefaultHasher::new();
slice.hash(&mut h);
h.finish()
}
fn group_size(generators: &[Vec<usize>], n: usize) -> u64 {
let mut seen: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
let mut queue: Vec<Vec<usize>> = Vec::new();
let id: Vec<usize> = (0..n).collect();
seen.insert(id.clone());
queue.push(id);
let inverses: Vec<Vec<usize>> = generators
.iter()
.map(|g| {
let mut inv = vec![0usize; n];
for (i, &img) in g.iter().enumerate() {
inv[img] = i;
}
inv
})
.collect();
let mut head = 0;
while head < queue.len() {
let cur = queue[head].clone();
head += 1;
for generator in generators {
let next: Vec<usize> = cur.iter().map(|&v| generator[v]).collect();
if seen.insert(next.clone()) {
queue.push(next);
}
}
for inv in &inverses {
let prev: Vec<usize> = cur.iter().map(|&v| inv[v]).collect();
if seen.insert(prev.clone()) {
queue.push(prev);
}
}
}
seen.len() as u64
}
#[derive(Debug, Clone)]
struct Partition {
cells: Vec<Vec<usize>>,
cell_of: Vec<usize>,
}
impl Partition {
fn from_graph<N, H, E>(g: &Graph<N, H, E>) -> Self
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
let n = g.nodes.len();
let mut groups: HashMap<N, Vec<usize>> = HashMap::new();
for (v, (data, _)) in g.nodes.iter().enumerate() {
groups.entry(data.clone()).or_default().push(v);
}
let mut cells: Vec<(N, Vec<usize>)> = groups.into_iter().collect();
cells.sort_by(|a, b| a.0.cmp(&b.0));
let cells: Vec<Vec<usize>> = cells
.into_iter()
.map(|(_, mut vs)| {
vs.sort();
vs
})
.collect();
let mut cell_of = vec![0usize; n];
for (ci, cell) in cells.iter().enumerate() {
for &v in cell {
cell_of[v] = ci;
}
}
Partition { cells, cell_of }
}
fn cell_lengths(&self) -> Vec<usize> {
self.cells.iter().map(|c| c.len()).collect()
}
fn is_discrete(&self) -> bool {
self.cells.iter().all(|c| c.len() == 1)
}
fn labeling(&self) -> Vec<usize> {
self.cells.iter().flatten().copied().collect()
}
fn smallest_nontrivial_cell(&self) -> Option<usize> {
let mut best: Option<(usize, usize)> = None; for (i, cell) in self.cells.iter().enumerate() {
if cell.len() <= 1 {
continue;
}
match best {
None => best = Some((i, cell.len())),
Some((_, best_len)) if cell.len() < best_len => {
best = Some((i, cell.len()));
}
_ => {}
}
}
best.map(|(i, _)| i)
}
fn individualize(&mut self, cell_idx: usize, vpos: usize) {
let v = self.cells[cell_idx].remove(vpos);
self.cells.insert(cell_idx, vec![v]);
self.cell_of[v] = cell_idx;
for ci in (cell_idx + 1)..self.cells.len() {
for &w in &self.cells[ci] {
self.cell_of[w] = ci;
}
}
if self.cells[cell_idx + 1].is_empty() {
self.cells.remove(cell_idx + 1);
for ci in (cell_idx + 1)..self.cells.len() {
for &w in &self.cells[ci] {
self.cell_of[w] = ci;
}
}
}
}
fn refine<N, H, E>(&mut self, g: &Graph<N, H, E>)
where
N: Clone + Debug + Eq + Hash + Ord,
H: Clone + Debug,
E: Clone + Debug + Eq + Hash + Ord,
{
let num_cells = self.cells.len();
if num_cells <= 1 {
return;
}
let mut stable_below = 0usize;
'outer: while stable_below < self.cells.len() {
let mut i = stable_below;
while i < self.cells.len() {
if self.cells[i].len() <= 1 {
i += 1;
continue;
}
let mut split = false;
for j in 0..self.cells.len() {
if i == j {
continue;
}
let sigs = cell_signatures(
&self.cells[i],
j,
&self.cell_of,
&g.adj_out,
&g.adj_in,
&g.edges,
);
if sigs.len() > 1 {
let new_cells: Vec<Vec<usize>> =
sigs.into_iter().map(|(_, vs)| vs).collect();
let _old_cell = std::mem::take(&mut self.cells[i]);
self.cells.splice(i..=i, new_cells);
for (offset, cell) in self.cells[i..].iter().enumerate() {
for &v in cell {
self.cell_of[v] = i + offset;
}
}
stable_below = i;
split = true;
break;
}
}
if !split {
i += 1;
}
if split {
continue 'outer;
}
}
break;
}
}
}
#[allow(clippy::type_complexity)]
fn cell_signatures<E: Clone + Debug + Eq + Hash + Ord>(
cell: &[usize],
target_cell_idx: usize,
cell_of: &[usize],
adj_out: &[Vec<(usize, usize)>],
adj_in: &[Vec<(usize, usize)>],
edges: &[Edge<E>],
) -> Vec<(Vec<(E, u8)>, Vec<usize>)> {
let mut groups: Vec<(Vec<(E, u8)>, Vec<usize>)> = Vec::new();
for &v in cell {
let sig = vertex_signature(v, target_cell_idx, cell_of, adj_out, adj_in, edges);
let pos = groups.iter().position(|(s, _)| *s == sig);
match pos {
Some(idx) => groups[idx].1.push(v),
None => groups.push((sig, vec![v])),
}
}
groups
}
fn vertex_signature<E: Clone + Debug + Eq + Hash + Ord>(
v: usize,
target_cell_idx: usize,
cell_of: &[usize],
adj_out: &[Vec<(usize, usize)>],
adj_in: &[Vec<(usize, usize)>],
edges: &[Edge<E>],
) -> Vec<(E, u8)> {
let mut sig: Vec<(E, u8)> = Vec::new();
for &(edge_idx, other) in &adj_out[v] {
if cell_of[other] == target_cell_idx {
let edge = &edges[edge_idx];
let dir = if edge.directed {
if edge.from == v { 0u8 } else { 1u8 }
} else {
2u8
};
sig.push((edge.data.clone(), dir));
}
}
for &(edge_idx, other) in &adj_in[v] {
if cell_of[other] == target_cell_idx {
sig.push((edges[edge_idx].data.clone(), 1u8));
}
}
sig.sort();
sig
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Invariant {
path: Vec<Vec<usize>>,
}
impl Invariant {
fn from_partition(part: &Partition, _n: usize) -> Self {
Invariant {
path: vec![part.cell_lengths()],
}
}
fn extend(&self, cell_lengths: Vec<usize>) -> Self {
let mut path = self.path.clone();
path.push(cell_lengths);
Invariant { path }
}
}
#[derive(Debug, Clone)]
struct SearchFrame {
partition: Partition,
invariant: Invariant,
is_leftmost: bool,
}
fn compose_permutations(labeling_a: &[usize], labeling_b: &[usize]) -> Vec<usize> {
let n = labeling_a.len();
let mut inv_a = vec![0usize; n];
for (pos, &v) in labeling_a.iter().enumerate() {
inv_a[v] = pos;
}
let mut inv_b = vec![0usize; n];
for (pos, &v) in labeling_b.iter().enumerate() {
inv_b[v] = pos;
}
let mut perm = vec![0usize; n];
for i in 0..n {
perm[i] = inv_b[labeling_a[i]];
}
perm
}
fn orbit_prune(
v: usize,
processed: &[usize],
generators: &[Vec<usize>],
_cell_idx: usize,
_partition: &Partition,
) -> bool {
if processed.is_empty() || generators.is_empty() {
return false;
}
let n = if let Some(g1) = generators.first() {
g1.len()
} else {
return false;
};
for &p in processed {
let mut orbit = vec![false; n];
let mut queue = vec![p];
orbit[p] = true;
let mut head = 0;
while head < queue.len() {
let cur = queue[head];
head += 1;
for generator in generators {
let next = generator[cur];
if !orbit[next] {
orbit[next] = true;
queue.push(next);
}
}
}
if orbit[v] {
return true;
}
}
false
}
fn compute_orbits(generators: &[Vec<usize>], n: usize) -> Vec<Vec<usize>> {
if generators.is_empty() {
return (0..n).map(|i| vec![i]).collect();
}
let inverses: Vec<Vec<usize>> = generators
.iter()
.map(|generator| {
let mut inv = vec![0usize; n];
for (i, &img) in generator.iter().enumerate() {
inv[img] = i;
}
inv
})
.collect();
let mut visited = vec![false; n];
let mut orbits: Vec<Vec<usize>> = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
let mut orbit: Vec<usize> = Vec::new();
let mut queue = vec![start];
visited[start] = true;
while let Some(cur) = queue.pop() {
orbit.push(cur);
for generator in generators {
let next = generator[cur];
if !visited[next] {
visited[next] = true;
queue.push(next);
}
}
for inv in &inverses {
let prev = inv[cur];
if !visited[prev] {
visited[prev] = true;
queue.push(prev);
}
}
}
orbit.sort();
orbits.push(orbit);
}
orbits.sort_by_key(|o| o[0]);
orbits
}
#[cfg(test)]
mod tests {
use super::*;
fn trivial_vertices(n: usize) -> Graph<i32, (), ()> {
let mut g = Graph::new();
for _ in 0..n {
g.add_node(0, ());
}
g
}
#[test]
fn empty_graph() {
let g: Graph<i32, (), ()> = Graph::new();
let cf = g.canonize();
assert!(cf.vertex_map.is_empty());
assert_eq!(cf.automorphism_group_size, 1);
}
#[test]
fn single_vertex() {
let mut g = Graph::<i32, (), ()>::new();
g.add_node(42, ());
let cf = g.canonize();
assert_eq!(cf.vertex_map, vec![0]);
assert_eq!(cf.orbits.len(), 1);
assert_eq!(cf.automorphism_group_size, 1);
}
#[test]
fn two_isolated_vertices_same_colour() {
let g = trivial_vertices(2);
let cf = g.canonize();
assert_eq!(cf.orbits.len(), 1);
assert_eq!(cf.orbits[0].len(), 2);
assert_eq!(cf.automorphism_group_size, 2);
}
#[test]
fn three_isolated_vertices_same_colour() {
let g = trivial_vertices(3);
let cf = g.canonize();
assert_eq!(cf.orbits.len(), 1);
assert_eq!(cf.orbits[0].len(), 3);
assert_eq!(cf.automorphism_group_size, 6);
}
#[test]
fn different_colours_not_swappable() {
let mut g = Graph::<i32, (), ()>::new();
g.add_node(1, ());
g.add_node(2, ());
let cf = g.canonize();
assert_eq!(cf.orbits.len(), 2);
assert_eq!(cf.automorphism_group_size, 1);
}
#[test]
fn directed_edge_preserves_direction() {
let mut g = Graph::<i32, (), i32>::new();
let a = g.add_node(0, ());
let b = g.add_node(0, ());
g.add_directed_edge(a, b, 1);
let cf = g.canonize();
assert_eq!(cf.orbits.len(), 2);
assert_eq!(cf.automorphism_group_size, 1);
}
#[test]
fn undirected_edge_makes_vertices_equivalent() {
let mut g = Graph::<i32, (), i32>::new();
let a = g.add_node(0, ());
let b = g.add_node(0, ());
g.add_undirected_edge(a, b, 1);
let cf = g.canonize();
assert_eq!(cf.orbits.len(), 1);
assert!(cf.automorphism_group_size >= 1);
}
#[test]
fn cycle_4_automorphism_d8() {
let mut g = Graph::<i32, (), ()>::new();
let v: Vec<usize> = (0..4).map(|_| g.add_node(0, ())).collect();
g.add_undirected_edge(v[0], v[1], ());
g.add_undirected_edge(v[1], v[2], ());
g.add_undirected_edge(v[2], v[3], ());
g.add_undirected_edge(v[3], v[0], ());
let cf = g.canonize();
let mut sorted: Vec<usize> = cf.vertex_map.clone();
sorted.sort();
assert_eq!(sorted, (0..4).collect::<Vec<_>>());
assert_eq!(cf.graph.node_count(), 4);
}
#[test]
fn relabeling_invariance_proptest_style() {
let mut g = Graph::<i32, (), i32>::new();
let v0 = g.add_node(1, ()); let v1 = g.add_node(2, ()); let v2 = g.add_node(2, ()); let v3 = g.add_node(1, ()); g.add_undirected_edge(v0, v1, 10);
g.add_directed_edge(v1, v2, 20);
g.add_undirected_edge(v2, v3, 30);
let cf1 = g.canonize();
let mut g2 = Graph::<i32, (), i32>::new();
let w1 = g2.add_node(2, ()); let w2 = g2.add_node(2, ()); let w0 = g2.add_node(1, ()); let w3 = g2.add_node(1, ()); g2.add_undirected_edge(w0, w1, 10); g2.add_directed_edge(w1, w2, 20); g2.add_undirected_edge(w2, w3, 30);
let cf2 = g2.canonize();
let canon1_nodes: Vec<i32> = (0..cf1.graph.node_count())
.map(|i| *cf1.graph.node_data(i))
.collect();
let canon2_nodes: Vec<i32> = (0..cf2.graph.node_count())
.map(|i| *cf2.graph.node_data(i))
.collect();
assert_eq!(canon1_nodes, canon2_nodes, "canonical node colours differ");
assert_eq!(
cf1.automorphism_group_size, cf2.automorphism_group_size,
"automorphism group sizes differ"
);
}
#[test]
fn stress_64_vertex_random_graph() {
let mut g = Graph::<i32, (), ()>::new();
for i in 0..64 {
g.add_node(i % 4, ()); }
for i in 0..64 {
let a = i;
let b = (i + 1) % 64;
let c = (i + 3) % 64;
let d = (i + 7) % 64;
g.add_undirected_edge(a, b, ());
if i % 2 == 0 {
g.add_directed_edge(a, c, ());
} else {
g.add_undirected_edge(a, c, ());
}
g.add_undirected_edge(a, d, ());
}
let cf = g.canonize();
let mut sorted: Vec<usize> = cf.vertex_map.clone();
sorted.sort();
assert_eq!(sorted, (0..64).collect::<Vec<_>>());
assert_eq!(cf.graph.node_count(), 64);
}
}