blob: a51371b39fa732011bf0538b382d6296a1175276 [file] [log] [blame]
use core::hash::Hash;
use std::vec::Vec;
use indexmap::IndexSet;
use petgraph_algorithms::heuristics::{greedy_matching, maximum_matching, Matching};
use petgraph_core::visit::{
EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeCount, NodeIndexable, VisitMap, Visitable,
};
use petgraph_graph::{stable::StableUnGraph, NodeIndex, UnGraph};
use proptest::prelude::*;
macro_rules! assert_one_of {
($actual:expr, [$($expected:expr),+]) => {
let expected = &[$($expected),+];
if !expected.iter().any(|expected| expected == &$actual) {
let expected = expected.iter().map(|e| format!("\n{:?}", e)).collect::<Vec<_>>();
let comma_separated = expected.join(", ");
panic!("assertion failed: `actual does not equal to any of expected`\nactual:\n{:?}\nexpected:{}", $actual, comma_separated);
}
};
}
macro_rules! set {
() => {
IndexSet::new()
};
($(($source:expr, $target:expr)),+) => {
{
let mut set = IndexSet::new();
$(
set.insert(($source.into(), $target.into()));
)*
set
}
};
($($elem:expr),+) => {
{
let mut set = IndexSet::new();
$(
set.insert($elem.into());
)*
set
}
};
}
// So we don't have to type `.collect::<HashSet<_>>`.
fn collect<'a, T: Copy + Eq + Hash + 'a>(iter: impl Iterator<Item = T>) -> IndexSet<T> {
iter.collect()
}
#[test]
fn greedy_empty() {
let graph: UnGraph<(), ()> = UnGraph::default();
let matching = greedy_matching(&graph);
assert_eq!(collect(matching.edges()), set![]);
assert_eq!(collect(matching.nodes()), set![]);
}
#[test]
fn greedy_disjoint() {
let graph: UnGraph<(), ()> = UnGraph::from_edges([
(0, 1), //
(2, 3),
]);
let matching = greedy_matching(&graph);
assert_eq!(collect(matching.edges()), set![(0, 1), (2, 3)]);
assert_eq!(collect(matching.nodes()), set![0, 1, 2, 3]);
}
#[test]
fn greedy_odd_path() {
let graph: UnGraph<(), ()> = UnGraph::from_edges([
(0, 1), //
(1, 2),
(2, 3),
]);
let matching = greedy_matching(&graph);
assert_one_of!(collect(matching.edges()), [
set![(0, 1), (2, 3)], //
set![(1, 2)]
]);
assert_one_of!(collect(matching.nodes()), [set![0, 1, 2, 3], set![1, 2]]);
}
#[test]
fn greedy_star() {
let graph: UnGraph<(), ()> = UnGraph::from_edges([
(0, 1), //
(0, 2),
(0, 3),
]);
let matching = greedy_matching(&graph);
assert_one_of!(collect(matching.edges()), [
set![(0, 1)],
set![(0, 2)],
set![(0, 3)]
]);
assert_one_of!(collect(matching.nodes()), [set![0, 1], set![0, 2], set![
0, 3
]]);
}
#[test]
fn maximum_empty() {
let graph: UnGraph<(), ()> = UnGraph::default();
let matching = maximum_matching(&graph);
assert_eq!(collect(matching.edges()), set![]);
assert_eq!(collect(matching.nodes()), set![]);
}
#[test]
fn maximum_disjoint() {
let graph: UnGraph<(), ()> = UnGraph::from_edges([
(0, 1), //
(2, 3),
]);
let matching = maximum_matching(&graph);
assert_eq!(collect(matching.edges()), set![(0, 1), (2, 3)]);
assert_eq!(collect(matching.nodes()), set![0, 1, 2, 3]);
}
#[test]
fn maximum_odd_path() {
let graph: UnGraph<(), ()> = UnGraph::from_edges([
(0, 1), //
(1, 2),
(2, 3),
]);
let matching = maximum_matching(&graph);
assert_eq!(collect(matching.edges()), set![(0, 1), (2, 3)]);
assert_eq!(collect(matching.nodes()), set![0, 1, 2, 3]);
}
#[test]
fn maximum_in_stable_graph() {
let mut graph: StableUnGraph<(), ()> = StableUnGraph::from_edges([
(0, 1), //
(0, 2),
(1, 2),
(1, 3),
(2, 4),
(3, 4),
(3, 5),
]);
// Create a hole by removing node that would otherwise belong to the maximum
// matching.
graph.remove_node(NodeIndex::new(4));
let matching = maximum_matching(&graph);
assert_one_of!(collect(matching.edges()), [
set![(0, 1), (3, 5)],
set![(0, 2), (1, 3)],
set![(0, 2), (3, 5)]
]);
assert_one_of!(collect(matching.nodes()), [
set![0, 1, 3, 5],
set![0, 2, 1, 3],
set![0, 2, 3, 5]
]);
}
#[test]
fn is_perfect_in_stable_graph() {
let mut graph: StableUnGraph<(), ()> = StableUnGraph::from_edges([
(0, 1), //
(1, 2),
(2, 3),
]);
graph.remove_node(NodeIndex::new(0));
graph.remove_node(NodeIndex::new(1));
let matching = maximum_matching(&graph);
assert_eq!(matching.len(), 1);
assert!(matching.is_perfect());
}
fn is_valid_matching<G>(matching: &Matching<G>) -> bool
where
G: NodeIndexable,
{
// A set of edges is a matching if no two edges from the matching share an
// endpoint.
for (s1, t1) in matching.edges() {
for (s2, t2) in matching.edges() {
if s1 == s2 && t1 == t2 {
continue;
}
if s1 == s2 || s1 == t2 || t1 == s2 || t1 == t2 {
// Two edges share an endpoint.
return false;
}
}
}
true
}
fn is_maximum_matching<G>(graph: G, matching: &Matching<G>) -> bool
where
G: NodeIndexable + IntoEdges + IntoNodeIdentifiers + Visitable,
{
// Berge's lemma: a matching is maximum iff there is no augmenting path (a
// path that starts and ends in unmatched vertices, and alternates between
// matched and unmatched edges). Thus if we find an augmenting path, the
// matching is not maximum.
//
// Start with an unmatched node and traverse the graph alternating matched
// and unmatched edges. If an unmatched node is found, then an augmenting
// path was found.
for unmatched in graph
.node_identifiers()
.filter(|u| !matching.contains_node(*u))
{
let visited = &mut graph.visit_map();
let mut stack = Vec::new();
stack.push((unmatched, false));
while let Some((u, do_matched_edges)) = stack.pop() {
if visited.visit(u) {
for e in graph.edges(u) {
if e.source() == e.target() {
// Ignore self-loops.
continue;
}
let is_matched = matching.contains_edge(e.source(), e.target());
if do_matched_edges && is_matched || !do_matched_edges && !is_matched {
stack.push((e.target(), !do_matched_edges));
// Found another free node (other than the starting one)
// that is unmatched - an augmenting path.
if !is_matched
&& !matching.contains_node(e.target())
&& e.target() != unmatched
{
return false;
}
}
}
}
}
}
true
}
fn is_perfect_matching<G>(g: G, m: &Matching<G>) -> bool
where
G: NodeCount + NodeIndexable,
{
// By definition.
g.node_count() % 2 == 0 && m.edges().count() == g.node_count() / 2
}
#[cfg(not(miri))]
proptest! {
#[test]
fn greedy_matching_is_valid(graph: StableUnGraph<(), (), u8>) {
let matching = greedy_matching(&graph);
prop_assert!(is_valid_matching(&matching));
}
#[test]
fn maximum_matching_is_valid(graph: StableUnGraph<(), (), u8>) {
let matching = maximum_matching(&graph);
prop_assert!(is_valid_matching(&matching));
}
#[test]
fn maximum_matching_is_maximum(graph: StableUnGraph<(), (), u8>) {
let matching = maximum_matching(&graph);
prop_assert!(is_maximum_matching(&graph, &matching));
}
#[test]
fn greedy_matching_is_maximum(graph: StableUnGraph<(), (), u8>) {
let matching = greedy_matching(&graph);
prop_assert_eq!(matching.is_perfect(), is_perfect_matching(&graph, &matching));
}
#[test]
fn maximum_matching_is_perfect(graph: StableUnGraph<(), (), u8>) {
let matching = maximum_matching(&graph);
prop_assert_eq!(matching.is_perfect(), is_perfect_matching(&graph, &matching));
}
}