From: Jeff Hemminger Date: Thu, 8 Jan 2026 01:38:51 +0000 (+0900) Subject: added quickcheck and leftist heap impl X-Git-Url: http://gitweb.hemminger.haus/?a=commitdiff_plain;h=a5984d98962ea171bba4b17844235b3c178e078b;p=algorithms.git added quickcheck and leftist heap impl --- diff --git a/.gitignore b/.gitignore index 2a0038a..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -.idea \ No newline at end of file diff --git a/src/algorithms/heap/leftist_priority_heap.rs b/src/algorithms/heap/leftist_priority_heap.rs index d31397e..d5de02e 100644 --- a/src/algorithms/heap/leftist_priority_heap.rs +++ b/src/algorithms/heap/leftist_priority_heap.rs @@ -83,28 +83,27 @@ impl Heap { _ => unreachable!(), }; - if is_h1_smaller { - if let Heap::Node { - elem, left, right, .. - } = h1 - { - let left_heap = Self::unwrap_or_clone(left); - let right_heap = Self::unwrap_or_clone(right); - Self::make_node(elem, left_heap, right_heap.merge(h2)) - } else { - unreachable!() - } + // 1. The Isomorphism: Swap (h1, h2) -> (root, other) based on ordering + // This eliminates the branching logic by normalizing the data inputs. + let (root, other) = if is_h1_smaller { (h1, h2) } else { (h2, h1) }; + + // 2. Unify the Logic + if let Heap::Node { + elem, left, right, .. + } = root + { + let left_heap = Self::unwrap_or_clone(left); + let right_heap = Self::unwrap_or_clone(right); + + // 3. Currying / Partial Application + // We create a closure `node_builder` representing the function: Heap -> Heap + // It captures the fixed context (elem, left) and waits for the new right branch. + let node_builder = |new_right| Self::make_node(elem, left_heap, new_right); + + // 4. Evaluation + node_builder(right_heap.merge(other)) } else { - if let Heap::Node { - elem, left, right, .. - } = h2 - { - let left_heap = Self::unwrap_or_clone(left); - let right_heap = Self::unwrap_or_clone(right); - Self::make_node(elem, left_heap, h1.merge(right_heap)) - } else { - unreachable!() - } + unreachable!() } } } diff --git a/src/algorithms/interview_scheduling/earliest_completion.rs b/src/algorithms/interview_scheduling/earliest_completion.rs index 1fd7656..367330f 100644 --- a/src/algorithms/interview_scheduling/earliest_completion.rs +++ b/src/algorithms/interview_scheduling/earliest_completion.rs @@ -7,7 +7,7 @@ where #[derive(Debug, Clone, PartialEq, Eq)] pub struct Interview where - T: Ord + Copy, // ← Add this constraint + T: Ord + Copy, { pub start: Time, pub end: Time, @@ -16,7 +16,7 @@ where impl Interview where - T: Ord + Copy, // ← Also add here for impl blocks + T: Ord + Copy, { pub fn new(id: usize, start: T, end: T) -> Self { Self { @@ -69,24 +69,86 @@ where #[cfg(test)] mod tests { + use super::*; // Import Time, Interview, schedule_interviews_optimal + use quickcheck_macros::quickcheck; + use quickcheck::{Arbitrary, Gen}; + + // Implement Arbitrary for Interview + impl Arbitrary for Interview { + fn arbitrary(g: &mut Gen) -> Self { + let start = (i32::arbitrary(g) % 1000).max(0); // ensure non-negative + let duration = usize::arbitrary(g) % 30 + 1; // 1-30 duration + let end = start + duration as i32; + Interview::new(usize::arbitrary(g), start, end) + } + + // this function minimizes counterexamples for debugging. + // e.g.: + // Input: [Interview{start:0,end:60,id:42}] + // Fails → shrink → [Interview{start:0,end:30,id:42}] + // Fails → shrink → [Interview{start:0,end:15,id:42}] + // Fails → shrink → [Interview{start:0,end:7,id:42}] + // ... minimal case: [Interview{start:0,end:1,id:42}] + fn shrink(&self) -> Box> { + let duration = (self.end.0 - self.start.0).max(1); + Box::new(std::iter::once(Interview::new( + self.id, + self.start.0, + self.start.0 + (duration / 2), + ))) + } + } + + // Property 1: Selected interviews are non-overlapping + #[quickcheck] + fn prop_non_overlapping(interviews: Vec>) -> bool { + let selected = schedule_interviews_optimal(interviews); + (0..selected.len()).all(|i| { + (i + 1..selected.len()).all(|j| { + let a = &selected[i]; + let b = &selected[j]; + a.end <= b.start // No overlap: a ends before b starts + }) + }) + } + + // Property 2: Selected interviews are from original set + #[quickcheck] + fn prop_subset_of_original(interviews: Vec>) -> bool { + let selected = schedule_interviews_optimal(interviews.clone()); + selected.iter().all(|s| interviews.contains(s)) + } - use crate::interview_scheduling::test_data::*; + // Property 3: Sorting by end time produces correct order + #[quickcheck] + fn prop_sorted_by_end(mut interviews: Vec>) -> bool { + let orig_len = interviews.len(); + Interview::sort_by_earliest_completion(&mut interviews); + + // Check sorted by end time AND original length preserved + //interviews.windows(2).all(|w| w.end <= w[1].end) && interviews.len() == orig_len + interviews.windows(2).all(|w| w[0].end <= w[1].end) && interviews.len() == orig_len + } - #[test] - fn test_with_generated_data() { - let config = InterviewGenConfig { - count: 5, - min_start: 0, - max_start: 50, - min_duration: 5, - max_duration: 15, - seed: Some(42), - }; + // Property 4: Algorithm is idempotent + #[quickcheck] + fn prop_idempotent(interviews: Vec>) -> bool { + let result1 = schedule_interviews_optimal(interviews.clone()); + let result2 = schedule_interviews_optimal(result1.clone()); + result1 == result2 + } - let interviews = generate_random_interviews(&config); - assert_eq!(interviews.len(), 5); + // Property 5: Empty input gives empty output + #[quickcheck] + fn prop_empty_input() -> bool { + let empty: Vec> = vec![]; + schedule_interviews_optimal(empty).is_empty() + } - // Test your algorithm with this data - // let result = your_scheduling_algorithm(&interviews); + // Property 6: Single interview is always selected + #[quickcheck] + fn prop_single_always_selected(interview: Interview) -> bool { + let result = schedule_interviews_optimal(vec![interview.clone()]); + result.len() == 1 && result[0] == interview } } diff --git a/src/algorithms/stable_matching/bipartite.rs b/src/algorithms/stable_matching/bipartite.rs index 0fe9e62..fd82475 100644 --- a/src/algorithms/stable_matching/bipartite.rs +++ b/src/algorithms/stable_matching/bipartite.rs @@ -1,26 +1,6 @@ +use crate::gale_shapley::{Acceptor, AgentId, Proposer}; use std::collections::{HashMap, HashSet}; -/// Represents preference orderings as a total order on node IDs. -/// This is a simple wrapper that encodes a ranked preference list. -#[derive(Clone, Debug)] -pub struct Preferences { - pub ordered_ids: Vec, -} - -impl Preferences { - pub fn new(ordered_ids: Vec) -> Self { - Preferences { ordered_ids } - } - - /// Check if `a` is preferred over `b` in this preference order. - /// Returns None if either ID is not in the preference list. - pub fn prefers(&self, a: &K, b: &K) -> Option { - let pos_a = self.ordered_ids.iter().position(|id| id == a)?; - let pos_b = self.ordered_ids.iter().position(|id| id == b)?; - Some(pos_a < pos_b) - } -} - /// Trait for determining bipartite compatibility. /// Defines a morphism from L × R → Hom(*, Ω) where Ω is the two-element Boolean algebra. pub trait Compatible { @@ -32,14 +12,20 @@ pub trait Compatible { /// - `K`: Key type for identifying nodes (must be Hash + Eq + Clone) /// - `L`: Left partition node type /// - `R`: Right partition node type -pub struct BipartiteGraph { +pub struct BipartiteGraph +where + K: Clone + Eq + std::hash::Hash, +{ pub left_partition: HashMap, pub right_partition: HashMap, edges_from_left: HashMap>, edges_from_right: HashMap>, } -impl BipartiteGraph { +impl BipartiteGraph +where + K: Clone + Eq + std::hash::Hash, +{ pub fn new(left_partition: HashMap, right_partition: HashMap) -> Self { BipartiteGraph { left_partition, @@ -58,7 +44,6 @@ impl BipartiteGraph { .contains_key(&right_id) .then(|| ()) .ok_or("Right node ID not found")?; - self.edges_from_left .entry(left_id.clone()) .or_insert_with(Vec::new) @@ -67,7 +52,6 @@ impl BipartiteGraph { .entry(right_id) .or_insert_with(Vec::new) .push(left_id); - Ok(()) } @@ -78,258 +62,292 @@ impl BipartiteGraph { pub fn neighbors_of_right(&self, right_id: &K) -> Option<&[K]> { self.edges_from_right.get(right_id).map(|v| v.as_slice()) } + + /// Get all edges as pairs (left_id, right_id) + pub fn edges(&self) -> Vec<(K, K)> { + self.edges_from_left + .iter() + .flat_map(|(left_id, right_ids)| { + right_ids + .iter() + .map(move |right_id| (left_id.clone(), right_id.clone())) + }) + .collect() + } + + /// Check if a specific edge exists + pub fn has_edge(&self, left_id: &K, right_id: &K) -> bool { + self.edges_from_left + .get(left_id) + .map(|neighbors| neighbors.contains(right_id)) + .unwrap_or(false) + } } +/// Stable matching result: the bipartite graph itself, with edges representing matched pairs. +/// +/// The matching is embodied as a subgraph of the full bipartite graph where: +/// - Each left node (proposer) has degree ≤ 1 +/// - Each right node (acceptor) has degree ≤ 1 +/// - The matching is stable (no blocking pairs exist) +pub type StableMatchingGraph = BipartiteGraph; + /// Generic stable matching algorithm using the Gale-Shapley approach. /// -/// Type parameters: -/// - `K`: Key type for identifying nodes (must be Hash + Eq + Clone + Debug) -/// - `L`: Left partition node type -/// - `R`: Right partition node type +/// Modifies the input graph in place, adding edges to represent the stable matching. +/// The algorithm: +/// 1. Starts with all proposers free (no edges) +/// 2. Each step adds an edge to match a proposer with an acceptor +/// 3. Ends when all proposers are matched or have exhausted preferences /// -/// This function implements the Gale-Shapley algorithm as a fold operation, -/// viewing the matching process as successive refinements of a monoid structure -/// (matchings form a commutative monoid under compatibility). -pub fn stable_match( - graph: &BipartiteGraph, - left_prefs: &HashMap>, - right_prefs: &HashMap>, -) -> Result, &'static str> -where - K: Eq + std::hash::Hash + Clone + std::fmt::Debug, -{ - // Initialize state: all left nodes are free, no proposals yet made - let mut free_left: HashSet = graph.left_partition.keys().cloned().collect(); - let mut next_proposal: HashMap = HashMap::new(); - let mut matches: HashMap = HashMap::new(); // right_id -> left_id - - // Main loop: process free left nodes until none remain - while let Some(left_id) = free_left.iter().next().cloned() { - let prefs = left_prefs - .get(&left_id) - .ok_or("Left node has no preferences")?; - - let proposal_index = next_proposal.entry(left_id.clone()).or_insert(0); - - // If this left node has exhausted their preference list, remove them - if *proposal_index >= prefs.ordered_ids.len() { - free_left.remove(&left_id); - continue; - } +/// The input graph's edges encode the matching itself after completion. +pub fn stable_match( + graph: &mut BipartiteGraph, +) -> Result<(), &'static str> { + // Initialize state: all proposers are free + let mut free_left: HashSet = graph.left_partition.keys().cloned().collect(); + let mut proposers = graph.left_partition.clone(); + + // Track current engagements: acceptor_id -> proposer_id + let mut matches: HashMap = HashMap::new(); + + // Main loop: process free proposers until none remain + while let Some(proposer_id) = free_left.iter().next().cloned() { + let proposer = proposers + .get_mut(&proposer_id) + .ok_or("Proposer not found in graph")?; + + if let Some(acceptor_id) = proposer.next_proposal() { + // Verify acceptor exists in the graph + if !graph.right_partition.contains_key(&acceptor_id) { + return Err("Proposed acceptor does not exist"); + } - let right_id = prefs.ordered_ids[*proposal_index].clone(); - *proposal_index += 1; + let acceptor = graph + .right_partition + .get(&acceptor_id) + .ok_or("Acceptor not found in graph")?; - // Verify right node exists - if !graph.right_partition.contains_key(&right_id) { - return Err("Proposed right node does not exist"); - } + let current_partner = matches.get(&acceptor_id); - // Attempt the proposal - match matches.get(&right_id).cloned() { - // Case 1: Right node is unmatched - accept proposal - None => { - matches.insert(right_id, left_id.clone()); - free_left.remove(&left_id); - } - // Case 2: Right node is matched - check if they prefer the new proposer - Some(current_left_id) => { - let right_prefs = right_prefs - .get(&right_id) - .ok_or("Right node has no preferences")?; - - if right_prefs - .prefers(&left_id, ¤t_left_id) - .ok_or("Preference comparison failed")? - { - // Prefer new proposer: update match and free current match - matches.insert(right_id, left_id.clone()); - free_left.remove(&left_id); - free_left.insert(current_left_id); + if acceptor.prefers_over_current(current_partner, &proposer_id) { + // Free the old partner if one exists + if let Some(old_partner_id) = matches.get(&acceptor_id) { + free_left.insert(old_partner_id.clone()); } - // Else: reject proposal, keep left node free for next iteration + + // Record the new matching + matches.insert(acceptor_id.clone(), proposer_id.clone()); + + // Proposer is now engaged + free_left.remove(&proposer_id); + } else { + // Proposal rejected; proposer remains free for next iteration + free_left.remove(&proposer_id); + free_left.insert(proposer_id.clone()); } + } else { + // Proposer exhausted their preference list + free_left.remove(&proposer_id); } } - Ok(matches) -} - -// ============================================================================ -// Example Usage and Tests -// ============================================================================ + // Build the result graph by adding edges for all matches + for (acceptor_id, proposer_id) in matches { + graph.add_edge(proposer_id, acceptor_id)?; + } -#[derive(Clone, Debug)] -pub struct Person { - pub id: u32, - pub name: String, - pub gender: Gender, + Ok(()) } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Gender { - Male, - Female, -} +/// Verify stability of a matching (represented as a bipartite graph). +/// +/// A matching is stable if there exists no blocking pair (p, a) where: +/// - p and a are not matched +/// - p prefers a to their current match (if any) +/// - a prefers p to their current match (if any) +pub fn verify_stability(matching: &BipartiteGraph) -> bool { + // Build a map of current matches from edges + let matched_proposers: HashMap = matching + .edges() + .iter() + .map(|(p_id, a_id)| (p_id.clone(), a_id.clone())) + .collect(); + + let matched_acceptors: HashMap = matching + .edges() + .iter() + .map(|(p_id, a_id)| (a_id.clone(), p_id.clone())) + .collect(); + + // Check all pairs for blocking edges + for proposer in matching.left_partition.values() { + for acceptor in matching.right_partition.values() { + let p_id = &proposer.agent.id; + let a_id = &acceptor.agent.id; + + // Skip if already matched + if matched_proposers.get(p_id) == Some(a_id) { + continue; + } -pub struct GenderValidator; + let p_current = matched_proposers.get(p_id); + let a_current = matched_acceptors.get(a_id); -impl Compatible for GenderValidator { - fn can_connect(&self, left: &Person, right: &Person) -> bool { - left.gender != right.gender + // Check if proposer would prefer acceptor over current match + let p_prefers_a = p_current.map_or(true, |current| { + proposer.prefers(a_id, current).unwrap_or(false) + }); + // Check if acceptor would prefer proposer over current match + let a_prefers_p = acceptor.prefers_over_current(a_current, p_id); + + if p_prefers_a && a_prefers_p { + return false; // Blocking pair found + } + } } + + true } #[cfg(test)] mod tests { use super::*; + use crate::gale_shapley::StableMatchingInput; #[test] fn test_stable_match_simple() { - // Create preferences for a simple matching problem - let left_prefs: HashMap> = HashMap::from([ - (1, Preferences::new(vec![2, 3])), - (4, Preferences::new(vec![3, 2])), - ]); - - let right_prefs: HashMap> = HashMap::from([ - (2, Preferences::new(vec![1, 4])), - (3, Preferences::new(vec![4, 1])), - ]); - - // Create bipartite graph with Person nodes - let left_partition = HashMap::from([ - ( - 1, - Person { - id: 1, - name: "Alice".to_string(), - gender: Gender::Female, - }, - ), - ( - 4, - Person { - id: 4, - name: "Bob".to_string(), - gender: Gender::Female, - }, - ), - ]); - - let right_partition = HashMap::from([ - ( - 2, - Person { - id: 2, - name: "Charlie".to_string(), - gender: Gender::Male, - }, - ), - ( - 3, - Person { - id: 3, - name: "David".to_string(), - gender: Gender::Male, - }, - ), - ]); - - let graph: BipartiteGraph = + // Create input using the algebraic constructor + let proposer_ids = vec![AgentId("P1".to_string()), AgentId("P2".to_string())]; + let acceptor_ids = vec![AgentId("A1".to_string()), AgentId("A2".to_string())]; + + let proposer_prefs = vec![vec![0, 1], vec![1, 0]]; + let acceptor_prefs = vec![vec![0, 1], vec![1, 0]]; + + let input = + StableMatchingInput::new(proposer_ids, acceptor_ids, proposer_prefs, acceptor_prefs); + + // Create bipartite graph from the algebraic input + let left_partition: HashMap = input + .proposers + .iter() + .cloned() + .map(|p| (p.agent.id.clone(), p)) + .collect(); + + let right_partition: HashMap = input + .acceptors + .iter() + .cloned() + .map(|a| (a.agent.id.clone(), a)) + .collect(); + + let mut graph: BipartiteGraph = BipartiteGraph::new(left_partition, right_partition); - // Run stable matching algorithm - let result = stable_match::(&graph, &left_prefs, &right_prefs); - + // Run stable matching algorithm (modifies graph in place) + let result = stable_match(&mut graph); assert!(result.is_ok()); - let matches = result.unwrap(); - assert_eq!(matches.len(), 2); // Both should be matched - println!("Matches: {:?}", matches); + let edges = graph.edges(); + + assert_eq!(edges.len(), 2); // Both should be matched + assert!(verify_stability(&graph)); // Matching should be stable + + println!("Matching edges: {:?}", edges); } #[test] - fn test_stable_match_with_string_keys() { - // Demonstrate that the algorithm works with different key types - let left_prefs: HashMap> = HashMap::from([ - ( - "alice".to_string(), - Preferences::new(vec!["bob".to_string(), "charlie".to_string()]), - ), - ( - "diana".to_string(), - Preferences::new(vec!["charlie".to_string(), "bob".to_string()]), - ), - ]); - - let right_prefs: HashMap> = HashMap::from([ - ( - "bob".to_string(), - Preferences::new(vec!["alice".to_string(), "diana".to_string()]), - ), - ( - "charlie".to_string(), - Preferences::new(vec!["diana".to_string(), "alice".to_string()]), - ), - ]); - - let left_partition = HashMap::from([ - ( - "alice".to_string(), - Person { - id: 1, - name: "Alice".to_string(), - gender: Gender::Female, - }, - ), - ( - "diana".to_string(), - Person { - id: 2, - name: "Diana".to_string(), - gender: Gender::Female, - }, - ), - ]); - - let right_partition = HashMap::from([ - ( - "bob".to_string(), - Person { - id: 3, - name: "Bob".to_string(), - gender: Gender::Male, - }, - ), - ( - "charlie".to_string(), - Person { - id: 4, - name: "Charlie".to_string(), - gender: Gender::Male, - }, - ), - ]); - - let graph: BipartiteGraph = + fn test_unequal_sides() { + // 3 proposers, 2 acceptors + let proposer_ids = vec![ + AgentId("P1".to_string()), + AgentId("P2".to_string()), + AgentId("P3".to_string()), + ]; + let acceptor_ids = vec![AgentId("A1".to_string()), AgentId("A2".to_string())]; + + let proposer_prefs = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; + let acceptor_prefs = vec![vec![0, 1, 2], vec![1, 2, 0]]; + + let input = + StableMatchingInput::new(proposer_ids, acceptor_ids, proposer_prefs, acceptor_prefs); + + let left_partition: HashMap = input + .proposers + .iter() + .cloned() + .map(|p| (p.agent.id.clone(), p)) + .collect(); + + let right_partition: HashMap = input + .acceptors + .iter() + .cloned() + .map(|a| (a.agent.id.clone(), a)) + .collect(); + + let mut graph: BipartiteGraph = BipartiteGraph::new(left_partition, right_partition); - let result = stable_match::(&graph, &left_prefs, &right_prefs); - + let result = stable_match(&mut graph); assert!(result.is_ok()); - let matches = result.unwrap(); - assert_eq!(matches.len(), 2); - println!("String-keyed matches: {:?}", matches); + + let edges = graph.edges(); + + // At most 2 can be matched (limited by acceptors) + assert!(edges.len() <= 2); + assert!(verify_stability(&graph)); + + println!("Unequal sides matching edges: {:?}", edges); } #[test] - fn test_preference_order() { - let prefs = Preferences::new(vec![1, 2, 3, 4]); + fn test_matching_graph_structure() { + // Verify that the matching graph correctly embodies the matching + let proposer_ids = vec![AgentId("P1".to_string()), AgentId("P2".to_string())]; + let acceptor_ids = vec![AgentId("A1".to_string()), AgentId("A2".to_string())]; + + let proposer_prefs = vec![vec![0, 1], vec![1, 0]]; + let acceptor_prefs = vec![vec![0, 1], vec![1, 0]]; + + let input = + StableMatchingInput::new(proposer_ids, acceptor_ids, proposer_prefs, acceptor_prefs); + + let left_partition: HashMap = input + .proposers + .iter() + .cloned() + .map(|p| (p.agent.id.clone(), p)) + .collect(); + + let right_partition: HashMap = input + .acceptors + .iter() + .cloned() + .map(|a| (a.agent.id.clone(), a)) + .collect(); + + let mut graph: BipartiteGraph = + BipartiteGraph::new(left_partition, right_partition); + + stable_match(&mut graph).unwrap(); - assert_eq!(prefs.prefers(&1, &2), Some(true)); - assert_eq!(prefs.prefers(&2, &1), Some(false)); - assert_eq!(prefs.prefers(&1, &1), Some(false)); // Equal, not preferred - assert_eq!(prefs.prefers(&5, &1), None); // 5 not in list + // Verify degree constraints: each node has degree ≤ 1 + for proposer_id in graph.left_partition.keys() { + let degree = graph + .neighbors_of_left(proposer_id) + .map(|neighbors| neighbors.len()) + .unwrap_or(0); + assert!(degree <= 1, "Proposer has degree > 1"); + } + + for acceptor_id in graph.right_partition.keys() { + let degree = graph + .neighbors_of_right(acceptor_id) + .map(|neighbors| neighbors.len()) + .unwrap_or(0); + assert!(degree <= 1, "Acceptor has degree > 1"); + } } } diff --git a/src/algorithms/stable_matching/gale_shapley.rs b/src/algorithms/stable_matching/gale_shapley.rs index 67c18a4..5899a27 100644 --- a/src/algorithms/stable_matching/gale_shapley.rs +++ b/src/algorithms/stable_matching/gale_shapley.rs @@ -1,3 +1,4 @@ +use crate::StableMatchingGraph; use std::collections::HashMap; #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -38,6 +39,14 @@ impl Proposer { self.next_to_propose += 1; Some(proposal.clone()) } + + /// Query preference relationship: does proposer prefer `a` over `b`? + /// Returns None if either ID is not in the preference list. + pub fn prefers(&self, a: &AgentId, b: &AgentId) -> Option { + let pos_a = self.preferences.iter().position(|id| id == a)?; + let pos_b = self.preferences.iter().position(|id| id == b)?; + Some(pos_a < pos_b) + } } impl Acceptor { @@ -79,14 +88,46 @@ impl StableMatchingInput { proposer_prefs: Vec>, // indices into acceptor_ids acceptor_prefs: Vec>, // indices into proposer_ids ) -> Self { + // Validate size consistency + assert_eq!( + proposer_ids.len(), + proposer_prefs.len(), + "Proposer count mismatch" + ); + assert_eq!( + acceptor_ids.len(), + acceptor_prefs.len(), + "Acceptor count mismatch" + ); + + // Validate index bounds + for (i, prefs) in proposer_prefs.iter().enumerate() { + for &idx in prefs { + assert!( + idx < acceptor_ids.len(), + "Proposer {} preference contains invalid acceptor index {}", + i, + idx + ); + } + } + for (i, prefs) in acceptor_prefs.iter().enumerate() { + for &idx in prefs { + assert!( + idx < proposer_ids.len(), + "Acceptor {} preference contains invalid proposer index {}", + i, + idx + ); + } + } + let acceptors = acceptor_ids .iter() .zip(acceptor_prefs) .map(|(id, prefs)| { - let pref_agents: Vec = prefs - .iter() - .map(|&idx| acceptor_ids[idx].clone()) - .collect(); + let pref_agents: Vec = + prefs.iter().map(|&idx| proposer_ids[idx].clone()).collect(); Acceptor::new(id.clone(), pref_agents) }) .collect(); @@ -95,19 +136,19 @@ impl StableMatchingInput { .iter() .zip(proposer_prefs) .map(|(id, prefs)| { - let pref_agents: Vec = prefs - .iter() - .map(|&idx| acceptor_ids[idx].clone()) - .collect(); + let pref_agents: Vec = + prefs.iter().map(|&idx| acceptor_ids[idx].clone()).collect(); Proposer::new(id.clone(), pref_agents) }) .collect(); - Self { proposers, acceptors } + Self { + proposers, + acceptors, + } } } - pub fn gale_shapley(input: &StableMatchingInput) -> Vec<(AgentId, AgentId)> { let mut proposers = input.proposers.clone(); let acceptors = input.acceptors.clone(); @@ -128,7 +169,9 @@ pub fn gale_shapley(input: &StableMatchingInput) -> Vec<(AgentId, AgentId)> { let current_partner = engagements.get(&acceptor_id); if acceptor.prefers_over_current(current_partner, &proposer.agent.id) { - if let Some(old_partner_id) = engagements.insert(acceptor_id.clone(), proposer.agent.id.clone()) { + if let Some(old_partner_id) = + engagements.insert(acceptor_id.clone(), proposer.agent.id.clone()) + { let old_partner_idx = proposers .iter() .position(|p| p.agent.id == old_partner_id) @@ -147,13 +190,40 @@ pub fn gale_shapley(input: &StableMatchingInput) -> Vec<(AgentId, AgentId)> { .collect() } - +/// Gale–Shapley via the bipartite graph implementation. +/// Modifies the graph in place, returning it with edges representing the matching. +pub fn gale_shapley_via_bipartite( + input: &StableMatchingInput, +) -> Result { + use crate::bipartite::{BipartiteGraph, stable_match}; + use std::collections::HashMap; + + let left_partition: HashMap = input + .proposers + .iter() + .cloned() + .map(|p| (p.agent.id.clone(), p)) + .collect(); + + let right_partition: HashMap = input + .acceptors + .iter() + .cloned() + .map(|a| (a.agent.id.clone(), a)) + .collect(); + + let mut graph = BipartiteGraph::new(left_partition, right_partition); + stable_match(&mut graph)?; + Ok(graph) +} #[cfg(test)] mod tests { - use quickcheck_macros::quickcheck; - use crate::{Acceptor, AgentId, Proposer, gale_shapley, StableMatchingInput}; + use crate::{ + Acceptor, AgentId, Proposer, StableMatchingInput, gale_shapley, gale_shapley_via_bipartite, + }; use quickcheck::{Arbitrary, Gen}; + use quickcheck_macros::quickcheck; impl Arbitrary for AgentId { fn arbitrary(g: &mut Gen) -> Self { // Generate a simple ID from a number @@ -167,9 +237,8 @@ mod tests { let id = AgentId::arbitrary(g); // Generate a preference list of 3-10 other agents let pref_count = *g.choose(&[3, 4, 5, 6, 7, 8, 9, 10]).unwrap(); - let preferences: Vec = (0..pref_count) - .map(|_| AgentId::arbitrary(g)) - .collect(); + let preferences: Vec = + (0..pref_count).map(|_| AgentId::arbitrary(g)).collect(); Proposer::new(id, preferences) } @@ -180,9 +249,8 @@ mod tests { let id = AgentId::arbitrary(g); // Generate a preference list of 3-10 other agents let pref_count = *g.choose(&[3, 4, 5, 6, 7, 8, 9, 10]).unwrap(); - let preferences: Vec = (0..pref_count) - .map(|_| AgentId::arbitrary(g)) - .collect(); + let preferences: Vec = + (0..pref_count).map(|_| AgentId::arbitrary(g)).collect(); Acceptor::new(id, preferences) } @@ -208,7 +276,7 @@ mod tests { .collect(); let proposer_prefs: Vec> = (0..num_agents) - .map(|_| generate_permutation(g, num_agents)) // Full permutation + .map(|_| generate_permutation(g, num_agents)) // Full permutation .collect(); StableMatchingInput::new(proposer_ids, acceptor_ids, proposer_prefs, acceptor_prefs) @@ -231,7 +299,7 @@ mod tests { #[quickcheck] fn prop_complete_matching(input: StableMatchingInput) -> bool { - let matches = gale_shapley(&input); // Borrow with & + let matches = gale_shapley(&input); // Borrow with & let max_possible = input.proposers.len().min(input.acceptors.len()); matches.len() == max_possible } @@ -243,4 +311,20 @@ mod tests { // (simplified check - full stability requires more complex logic) true // Placeholder for full stability property } + #[quickcheck] + fn prop_both_implementations_agree(input: StableMatchingInput) -> bool { + use std::collections::HashSet; + + let m1: HashSet<_> = gale_shapley(&input).into_iter().collect(); + + let m2 = match gale_shapley_via_bipartite(&input) { + Ok(graph) => { + let edges: HashSet<_> = graph.edges().into_iter().collect(); + edges + } + Err(_) => return false, + }; + + m1 == m2 + } } diff --git a/src/algorithms/stable_matching/mod.rs b/src/algorithms/stable_matching/mod.rs index 5f3ad2b..00dfcd2 100644 --- a/src/algorithms/stable_matching/mod.rs +++ b/src/algorithms/stable_matching/mod.rs @@ -6,5 +6,7 @@ pub mod bipartite; pub mod gale_shapley; pub use bipartite::*; +pub use bipartite::{BipartiteGraph, StableMatchingGraph}; pub use gale_shapley::*; -pub use bipartite::Preferences; \ No newline at end of file + +// ===== ITERATOR HELPERS =====