use crate::chromatic::SimpleGraph;
use crate::error::{SpecialError, SpecialResult};
use std::collections::HashMap;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum Matroid {
Graphic(SimpleGraph),
Uniform {
n: usize,
rank: usize,
},
Transversal {
bipartite_edges: Vec<(usize, usize)>,
},
}
#[derive(Debug, Clone)]
pub struct TutteConfig {
pub max_elements: usize,
}
impl Default for TutteConfig {
fn default() -> Self {
TutteConfig { max_elements: 20 }
}
}
#[derive(Debug, Clone)]
pub struct TutteResult {
pub polynomial: HashMap<(usize, usize), i64>,
pub n_vertices: usize,
pub rank: usize,
pub n_elements: usize,
}
impl TutteResult {
pub fn new(n_vertices: usize, rank: usize, n_elements: usize) -> Self {
TutteResult {
polynomial: HashMap::new(),
n_vertices,
rank,
n_elements,
}
}
pub fn add_monomial(&mut self, x_pow: usize, y_pow: usize, coeff: i64) {
if coeff == 0 {
return;
}
let entry = self.polynomial.entry((x_pow, y_pow)).or_insert(0);
*entry += coeff;
if *entry == 0 {
self.polynomial.remove(&(x_pow, y_pow));
}
}
pub fn evaluate(&self, x: f64, y: f64) -> f64 {
let mut result = 0.0f64;
for (&(xi, yi), &coeff) in &self.polynomial {
result += coeff as f64 * x.powi(xi as i32) * y.powi(yi as i32);
}
result
}
pub fn chromatic_polynomial_at_k(&self, k: usize) -> i64 {
let n = self.n_vertices;
let x_val = 1.0 - k as f64;
let t_val = self.evaluate(x_val, 0.0);
let sign: f64 = if (n - 1).is_multiple_of(2) { 1.0 } else { -1.0 };
(sign * k as f64 * t_val).round() as i64
}
pub fn tutte_at_2_2(&self) -> i64 {
self.evaluate(2.0, 2.0).round() as i64
}
pub fn tutte_at_1_1(&self) -> i64 {
self.evaluate(1.0, 1.0).round() as i64
}
pub fn flow_polynomial_at_k(&self, k: usize) -> i64 {
let m = self.n_elements;
let n = self.n_vertices;
let cycle_rank = m - n + 1; let y_val = 1.0 - k as f64;
let t_val = self.evaluate(0.0, y_val);
let sign: f64 = if cycle_rank.is_multiple_of(2) {
1.0
} else {
-1.0
};
(sign * t_val).round() as i64
}
}
fn count_components(graph: &SimpleGraph) -> usize {
let n = graph.n_vertices;
if n == 0 {
return 0;
}
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut Vec<usize>, x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
fn union(parent: &mut Vec<usize>, x: usize, y: usize) {
let px = find(parent, x);
let py = find(parent, y);
if px != py {
parent[px] = py;
}
}
for &(u, v) in &graph.edges {
union(&mut parent, u, v);
}
let mut roots = std::collections::HashSet::new();
for i in 0..n {
roots.insert(find(&mut parent, i));
}
roots.len()
}
pub fn graphic_matroid_rank(graph: &SimpleGraph, edge_subset: &[usize]) -> usize {
let mut sub = SimpleGraph::new(graph.n_vertices);
for &idx in edge_subset {
if idx < graph.edges.len() {
let (u, v) = graph.edges[idx];
sub.edges.push((u, v));
}
}
let components = count_components(&sub);
graph.n_vertices.saturating_sub(components)
}
pub fn is_loop_in_tutte_graph(graph: &TutteGraph, edge_idx: usize) -> bool {
if edge_idx >= graph.edges.len() {
return false;
}
let (u, v) = graph.edges[edge_idx];
u == v
}
pub fn is_coloop_in_tutte_graph(graph: &TutteGraph, edge_idx: usize) -> bool {
if edge_idx >= graph.edges.len() {
return false;
}
let components_before = tutte_graph_components(graph);
let mut g_del = graph.clone();
g_del.edges.swap_remove(edge_idx);
let components_after = tutte_graph_components(&g_del);
components_after > components_before
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TutteGraph {
pub n_vertices: usize,
pub edges: Vec<(usize, usize)>,
}
impl TutteGraph {
pub fn from_simple(g: &SimpleGraph) -> Self {
TutteGraph {
n_vertices: g.n_vertices,
edges: g.edges.clone(),
}
}
fn canonical(&self) -> TutteGraph {
let mut edges = self.edges.clone();
for e in &mut edges {
if e.0 > e.1 {
std::mem::swap(&mut e.0, &mut e.1);
}
}
edges.sort_unstable();
TutteGraph {
n_vertices: self.n_vertices,
edges,
}
}
fn delete_edge(&self, idx: usize) -> TutteGraph {
let mut g = self.clone();
g.edges.swap_remove(idx);
g.canonical()
}
fn contract_edge(&self, idx: usize) -> TutteGraph {
let (u, v) = self.edges[idx];
if u == v {
return self.delete_edge(idx);
}
let remap = |x: usize| -> usize {
if x == v {
u
} else if x > v {
x - 1
} else {
x
}
};
let new_n = self.n_vertices - 1;
let mut new_edges: Vec<(usize, usize)> = Vec::new();
for (i, &(a, b)) in self.edges.iter().enumerate() {
if i == idx {
continue; }
let ra = remap(a);
let rb = remap(b);
let (ea, eb) = if ra <= rb { (ra, rb) } else { (rb, ra) };
new_edges.push((ea, eb));
}
new_edges.sort_unstable();
TutteGraph {
n_vertices: new_n,
edges: new_edges,
}
}
}
fn tutte_graph_components(graph: &TutteGraph) -> usize {
let n = graph.n_vertices;
if n == 0 {
return 0;
}
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut Vec<usize>, x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
for &(u, v) in &graph.edges {
if u != v {
let pu = find(&mut parent, u);
let pv = find(&mut parent, v);
if pu != pv {
parent[pu] = pv;
}
}
}
let mut roots = std::collections::HashSet::new();
for i in 0..n {
roots.insert(find(&mut parent, i));
}
roots.len()
}
fn poly_add(
a: &HashMap<(usize, usize), i64>,
b: &HashMap<(usize, usize), i64>,
) -> HashMap<(usize, usize), i64> {
let mut result = a.clone();
for (&key, &val) in b {
let entry = result.entry(key).or_insert(0);
*entry += val;
if *entry == 0 {
result.remove(&key);
}
}
result
}
fn poly_mul(
a: &HashMap<(usize, usize), i64>,
b: &HashMap<(usize, usize), i64>,
) -> HashMap<(usize, usize), i64> {
let mut result: HashMap<(usize, usize), i64> = HashMap::new();
for (&(xi, yi), &ca) in a {
for (&(xj, yj), &cb) in b {
let entry = result.entry((xi + xj, yi + yj)).or_insert(0);
*entry += ca * cb;
}
}
result.retain(|_, v| *v != 0);
result
}
fn tutte_rec(
graph: &TutteGraph,
memo: &mut HashMap<TutteGraph, HashMap<(usize, usize), i64>>,
) -> HashMap<(usize, usize), i64> {
let key = graph.canonical();
if let Some(cached) = memo.get(&key) {
return cached.clone();
}
let m = graph.edges.len();
if m == 0 {
let mut result = HashMap::new();
result.insert((0, 0), 1i64);
memo.insert(key, result.clone());
return result;
}
let mut loop_idx = None;
let mut coloop_idx = None;
let mut regular_idx = None;
for i in 0..m {
let is_lp = graph.edges[i].0 == graph.edges[i].1;
let is_cl = if !is_lp {
is_coloop_in_tutte_graph(graph, i)
} else {
false
};
if is_lp && loop_idx.is_none() {
loop_idx = Some(i);
} else if is_cl && coloop_idx.is_none() {
coloop_idx = Some(i);
} else if !is_lp && !is_cl && regular_idx.is_none() {
regular_idx = Some(i);
}
}
let result;
if let Some(li) = loop_idx {
let g_del = graph.delete_edge(li);
let t_del = tutte_rec(&g_del, memo);
let mut prod = HashMap::new();
for (&(xi, yi), &c) in &t_del {
prod.insert((xi, yi + 1), c);
}
result = prod;
} else if let Some(ci) = coloop_idx {
let g_del = graph.delete_edge(ci);
let t_del = tutte_rec(&g_del, memo);
let mut prod = HashMap::new();
for (&(xi, yi), &c) in &t_del {
prod.insert((xi + 1, yi), c);
}
result = prod;
} else {
let e_idx = regular_idx.unwrap_or(0);
let g_del = graph.delete_edge(e_idx);
let g_con = graph.contract_edge(e_idx);
let t_del = tutte_rec(&g_del, memo);
let t_con = tutte_rec(&g_con, memo);
result = poly_add(&t_del, &t_con);
}
memo.insert(key, result.clone());
result
}
fn tutte_uniform(n: usize, rank: usize) -> HashMap<(usize, usize), i64> {
let mut result: HashMap<(usize, usize), i64> = HashMap::new();
let expand_x_minus_1 = |m: usize| -> Vec<(usize, i64)> {
let mut terms = Vec::new();
for j in 0..=m {
let c = binomial_coeff(m, j);
let sign: i64 = if (m - j).is_multiple_of(2) { 1 } else { -1 };
terms.push((j, sign * c as i64));
}
terms
};
let expand_y_minus_1 = |m: usize| -> Vec<(usize, i64)> {
expand_x_minus_1(m) };
for k in 0..=n {
let binom_nk = binomial_coeff(n, k) as i64;
if k <= rank {
let exp = rank - k;
let x_terms = expand_x_minus_1(exp);
for (xi, c) in x_terms {
let entry = result.entry((xi, 0)).or_insert(0);
*entry += binom_nk * c;
}
} else {
let exp = k - rank;
let y_terms = expand_y_minus_1(exp);
for (yi, c) in y_terms {
let entry = result.entry((0, yi)).or_insert(0);
*entry += binom_nk * c;
}
}
}
result.retain(|_, v| *v != 0);
result
}
fn binomial_coeff(n: usize, k: usize) -> usize {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k);
let mut result = 1usize;
for i in 0..k {
result = result * (n - i) / (i + 1);
}
result
}
pub fn tutte_polynomial(matroid: &Matroid, config: &TutteConfig) -> SpecialResult<TutteResult> {
match matroid {
Matroid::Graphic(graph) => {
let m = graph.n_edges();
if m > config.max_elements {
return Err(SpecialError::ValueError(format!(
"Graph has {m} edges, exceeding max_elements={}",
config.max_elements
)));
}
let n = graph.n_vertices;
let rank = n - count_components(graph);
let tg = TutteGraph::from_simple(graph);
let mut memo: HashMap<TutteGraph, HashMap<(usize, usize), i64>> = HashMap::new();
let poly = tutte_rec(&tg, &mut memo);
let mut result = TutteResult::new(n, rank, m);
result.polynomial = poly;
Ok(result)
}
Matroid::Uniform { n, rank } => {
let r = *rank;
let n_val = *n;
if n_val > config.max_elements {
return Err(SpecialError::ValueError(format!(
"Uniform matroid has {n_val} elements, exceeding max_elements={}",
config.max_elements
)));
}
if r > n_val {
return Err(SpecialError::ValueError(format!(
"Rank {r} exceeds number of elements {n_val} in uniform matroid"
)));
}
let poly = tutte_uniform(n_val, r);
let mut result = TutteResult::new(n_val, r, n_val);
result.polynomial = poly;
Ok(result)
}
Matroid::Transversal { bipartite_edges } => {
let m = bipartite_edges.len();
if m > config.max_elements {
return Err(SpecialError::ValueError(format!(
"Transversal matroid has {m} edges, exceeding max_elements={}",
config.max_elements
)));
}
let max_left = bipartite_edges.iter().map(|e| e.0).max().unwrap_or(0);
let max_right = bipartite_edges.iter().map(|e| e.1).max().unwrap_or(0);
let n = max_left + max_right + 2;
let mut tg = TutteGraph {
n_vertices: n,
edges: Vec::new(),
};
for &(l, r) in bipartite_edges {
let rv = max_left + 1 + r;
let (u, v) = if l <= rv { (l, rv) } else { (rv, l) };
tg.edges.push((u, v));
}
tg = tg.canonical();
let rank = n - tutte_graph_components(&tg);
let mut memo: HashMap<TutteGraph, HashMap<(usize, usize), i64>> = HashMap::new();
let poly = tutte_rec(&tg, &mut memo);
let mut result = TutteResult::new(n, rank, m);
result.polynomial = poly;
Ok(result)
}
}
}
pub fn tutte_at_2_2(result: &TutteResult) -> i64 {
result.tutte_at_2_2()
}
pub fn tutte_at_1_1(result: &TutteResult) -> i64 {
result.tutte_at_1_1()
}
pub fn flow_polynomial(result: &TutteResult, k: usize) -> i64 {
result.flow_polynomial_at_k(k)
}
#[cfg(test)]
mod tests {
use super::*;
fn triangle() -> SimpleGraph {
let mut g = SimpleGraph::new(3);
g.add_edge(0, 1).expect("add edge");
g.add_edge(1, 2).expect("add edge");
g.add_edge(0, 2).expect("add edge");
g
}
fn path3() -> SimpleGraph {
let mut g = SimpleGraph::new(3);
g.add_edge(0, 1).expect("add edge");
g.add_edge(1, 2).expect("add edge");
g
}
#[test]
fn test_tutte_k3_spanning_trees() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let t11 = tutte_at_1_1(&result);
assert_eq!(t11, 3, "K_3 has 3 spanning trees, got {t11}");
}
#[test]
fn test_tutte_k3_chromatic_polynomial_at_2() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let p2 = result.chromatic_polynomial_at_k(2);
assert_eq!(p2, 0, "P(K_3, 2) = 0, got {p2}");
}
#[test]
fn test_tutte_k3_chromatic_polynomial_at_3() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let p3 = result.chromatic_polynomial_at_k(3);
assert_eq!(p3, 6, "P(K_3, 3) = 6, got {p3}");
}
#[test]
fn test_tutte_k3_flow_polynomial() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let f3 = flow_polynomial(&result, 3);
assert_eq!(f3, 2, "F(K_3, 3) = 2, got {f3}");
}
#[test]
fn test_tutte_path3_spanning_trees() {
let g = path3();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte P3");
let t11 = tutte_at_1_1(&result);
assert_eq!(t11, 1, "P_3 has 1 spanning tree, got {t11}");
}
#[test]
fn test_tutte_uniform_u1_2() {
let config = TutteConfig::default();
let result =
tutte_polynomial(&Matroid::Uniform { n: 2, rank: 1 }, &config).expect("uniform U12");
let t_xy = result.evaluate(2.0, 2.0);
assert!(t_xy > 0.0, "T(2,2) should be positive, got {t_xy}");
}
#[test]
fn test_tutte_uniform_u2_4() {
let config = TutteConfig::default();
let result =
tutte_polynomial(&Matroid::Uniform { n: 4, rank: 2 }, &config).expect("uniform U24");
let t22 = tutte_at_2_2(&result);
assert!(
t22 > 0,
"T(2,2) for U_{{2,4}} should be positive, got {t22}"
);
}
#[test]
fn test_tutte_too_large() {
let mut g = SimpleGraph::new(10);
for i in 0..9 {
g.add_edge(i, i + 1).expect("add edge");
}
for i in 0..9 {
for j in (i + 2)..10 {
let _ = g.add_edge(i, j); }
}
let config = TutteConfig { max_elements: 5 };
let result = tutte_polynomial(&Matroid::Graphic(g), &config);
assert!(result.is_err(), "Should fail for large graph");
}
#[test]
fn test_count_components_triangle() {
let g = triangle();
assert_eq!(count_components(&g), 1);
}
#[test]
fn test_graphic_matroid_rank_all_edges() {
let g = triangle();
let all_edges: Vec<usize> = (0..g.n_edges()).collect();
let r = graphic_matroid_rank(&g, &all_edges);
assert_eq!(r, 2, "Rank of K_3 = 2 (spanning tree has 2 edges)");
}
#[test]
fn test_tutte_polynomial_evaluate() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte");
let val = result.evaluate(1.0, 1.0);
assert!(
(val - 3.0).abs() < 0.5,
"T(1,1) for K_3 should be 3, got {val}"
);
}
#[test]
fn test_tutte_empty_graph() {
let g = SimpleGraph::new(3);
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte empty");
let val = result.evaluate(2.0, 2.0);
assert!(
(val - 1.0).abs() < 1e-10,
"Empty graph T(2,2) = 1, got {val}"
);
assert_eq!(
result.polynomial.get(&(0, 0)),
Some(&1i64),
"Empty graph has T = 1 as constant"
);
}
#[test]
fn test_tutte_single_edge() {
let mut g = SimpleGraph::new(2);
g.add_edge(0, 1).expect("add edge");
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte single edge");
let coeff_x = result.polynomial.get(&(1, 0)).copied().unwrap_or(0);
assert_eq!(
coeff_x, 1,
"Single bridge edge: coefficient of x should be 1, got {coeff_x}"
);
let total_terms = result.polynomial.len();
assert_eq!(
total_terms, 1,
"Single bridge: T = x has 1 term, got {total_terms}"
);
let val = result.evaluate(2.0, 2.0);
assert!(
(val - 2.0).abs() < 1e-10,
"T(2,2) single bridge = 2, got {val}"
);
let result_u12 =
tutte_polynomial(&Matroid::Uniform { n: 2, rank: 1 }, &config).expect("uniform U12");
let coeff_x_u = result_u12.polynomial.get(&(1, 0)).copied().unwrap_or(0);
let coeff_y_u = result_u12.polynomial.get(&(0, 1)).copied().unwrap_or(0);
assert_eq!(coeff_x_u, 1, "U12: coeff of x = 1");
assert_eq!(coeff_y_u, 1, "U12: coeff of y = 1");
}
#[test]
fn test_tutte_triangle_symbolic() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let coeff_x2 = result.polynomial.get(&(2, 0)).copied().unwrap_or(0);
let coeff_x1 = result.polynomial.get(&(1, 0)).copied().unwrap_or(0);
let coeff_y1 = result.polynomial.get(&(0, 1)).copied().unwrap_or(0);
assert_eq!(coeff_x2, 1, "K3: coeff of x^2 = 1, got {coeff_x2}");
assert_eq!(coeff_x1, 1, "K3: coeff of x = 1, got {coeff_x1}");
assert_eq!(coeff_y1, 1, "K3: coeff of y = 1, got {coeff_y1}");
assert_eq!(
result.polynomial.len(),
3,
"K3 Tutte polynomial has exactly 3 terms"
);
}
#[test]
fn test_tutte_bridge_factor() {
let g = path3();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte P3");
let coeff_x2 = result.polynomial.get(&(2, 0)).copied().unwrap_or(0);
assert_eq!(
coeff_x2, 1,
"P_3 (path): T = x^2, coeff of x^2 = 1, got {coeff_x2}"
);
let has_y = result.polynomial.keys().any(|&(_, yi)| yi > 0);
assert!(!has_y, "P_3 has no loop terms (no y factors in T)");
}
#[test]
fn test_tutte_loop_factor() {
let g = TutteGraph {
n_vertices: 1,
edges: vec![(0, 0)], };
let mut memo = std::collections::HashMap::new();
let poly = tutte_rec(&g, &mut memo);
let coeff_y = poly.get(&(0, 1)).copied().unwrap_or(0);
assert_eq!(
coeff_y, 1,
"Single loop: T = y, coeff of y = 1, got {coeff_y}"
);
assert_eq!(poly.len(), 1, "Single loop: T has exactly 1 term");
}
#[test]
fn test_tutte_chromatic_recovery() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
let p3 = result.chromatic_polynomial_at_k(3);
let p4 = result.chromatic_polynomial_at_k(4);
assert_eq!(p3, 6, "P(K_3, 3) = 6, got {p3}");
assert_eq!(p4, 24, "P(K_3, 4) = 24, got {p4}");
}
#[test]
fn test_tutte_coefficients_integer() {
let g = triangle();
let config = TutteConfig::default();
let result = tutte_polynomial(&Matroid::Graphic(g), &config).expect("tutte K3");
for (&(xi, yi), &coeff) in &result.polynomial {
assert!(
coeff > 0,
"Tutte coefficient at ({xi},{yi}) should be positive, got {coeff}"
);
}
}
#[test]
fn test_tutte_deletion_contraction() {
let g = triangle();
let config = TutteConfig::default();
let result_g = tutte_polynomial(&Matroid::Graphic(g.clone()), &config).expect("tutte G");
let mut g_del = SimpleGraph::new(3);
g_del.add_edge(0, 1).expect("add");
g_del.add_edge(1, 2).expect("add");
let result_del = tutte_polynomial(&Matroid::Graphic(g_del), &config).expect("tutte G\\e");
let g_con = TutteGraph {
n_vertices: 2,
edges: vec![(0, 1), (0, 1)], };
let mut memo = std::collections::HashMap::new();
let poly_con = tutte_rec(&g_con, &mut memo);
let t_g = result_g.evaluate(2.0, 2.0);
let t_del = result_del.evaluate(2.0, 2.0);
let t_con: f64 = poly_con
.iter()
.map(|(&(xi, yi), &c)| c as f64 * 2.0f64.powi(xi as i32) * 2.0f64.powi(yi as i32))
.sum();
assert!(
(t_g - (t_del + t_con)).abs() < 1.0,
"T(G) = T(G\\e) + T(G/e): {t_g} ≈ {t_del} + {t_con}"
);
}
}