]> localhost Git - algorithms.git/commitdiff
added quickcheck and leftist heap impl
authorJeff Hemminger <jeff@hemminger.haus>
Thu, 8 Jan 2026 01:38:51 +0000 (10:38 +0900)
committerJeff Hemminger <jeff@hemminger.haus>
Thu, 8 Jan 2026 01:38:51 +0000 (10:38 +0900)
.gitignore
src/algorithms/heap/leftist_priority_heap.rs
src/algorithms/interview_scheduling/earliest_completion.rs
src/algorithms/stable_matching/bipartite.rs
src/algorithms/stable_matching/gale_shapley.rs
src/algorithms/stable_matching/mod.rs

index 2a0038a460f6e43b38eaff116c8517a3a9f8e07c..ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba 100644 (file)
@@ -1,2 +1 @@
 /target
-.idea
\ No newline at end of file
index d31397ec33ffb04d552a17278b37836e83698d48..d5de02e348b428f26f3567bb91ada96aac18ad7a 100644 (file)
@@ -83,28 +83,27 @@ impl<T: Ord + Clone> Heap<T> {
                     _ => 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!()
                 }
             }
         }
index 1fd76568069297c16cd1d2b2b3888483e90fb520..367330f62b5bb047d45151cabb14e52a5ac87730 100644 (file)
@@ -7,7 +7,7 @@ where
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Interview<T>
 where
-    T: Ord + Copy, // ← Add this constraint
+    T: Ord + Copy,
 {
     pub start: Time<T>,
     pub end: Time<T>,
@@ -16,7 +16,7 @@ where
 
 impl<T> Interview<T>
 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<i32>
+    impl Arbitrary for Interview<i32> {
+        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<dyn Iterator<Item = Self>> {
+            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<Interview<i32>>) -> 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<Interview<i32>>) -> 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<Interview<i32>>) -> 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<Interview<i32>>) -> 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<Interview<i32>> = 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<i32>) -> bool {
+        let result = schedule_interviews_optimal(vec![interview.clone()]);
+        result.len() == 1 && result[0] == interview
     }
 }
index 0fe9e62398b4de133257b770cd37bba07f48fd5e..fd82475dedebd5e976750e63aa490b02056a26ce 100644 (file)
@@ -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<K> {
-    pub ordered_ids: Vec<K>,
-}
-
-impl<K: Clone + Eq> Preferences<K> {
-    pub fn new(ordered_ids: Vec<K>) -> 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<bool> {
-        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<L, R> {
@@ -32,14 +12,20 @@ pub trait Compatible<L, R> {
 /// - `K`: Key type for identifying nodes (must be Hash + Eq + Clone)
 /// - `L`: Left partition node type
 /// - `R`: Right partition node type
-pub struct BipartiteGraph<K: Eq + std::hash::Hash + Clone, L, R> {
+pub struct BipartiteGraph<K, L, R>
+where
+    K: Clone + Eq + std::hash::Hash,
+{
     pub left_partition: HashMap<K, L>,
     pub right_partition: HashMap<K, R>,
     edges_from_left: HashMap<K, Vec<K>>,
     edges_from_right: HashMap<K, Vec<K>>,
 }
 
-impl<K: Eq + std::hash::Hash + Clone, L, R> BipartiteGraph<K, L, R> {
+impl<K, L, R> BipartiteGraph<K, L, R>
+where
+    K: Clone + Eq + std::hash::Hash,
+{
     pub fn new(left_partition: HashMap<K, L>, right_partition: HashMap<K, R>) -> Self {
         BipartiteGraph {
             left_partition,
@@ -58,7 +44,6 @@ impl<K: Eq + std::hash::Hash + Clone, L, R> BipartiteGraph<K, L, R> {
             .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<K: Eq + std::hash::Hash + Clone, L, R> BipartiteGraph<K, L, R> {
             .entry(right_id)
             .or_insert_with(Vec::new)
             .push(left_id);
-
         Ok(())
     }
 
@@ -78,258 +62,292 @@ impl<K: Eq + std::hash::Hash + Clone, L, R> BipartiteGraph<K, L, R> {
     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<AgentId, Proposer, Acceptor>;
+
 /// 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<K, L, R>(
-    graph: &BipartiteGraph<K, L, R>,
-    left_prefs: &HashMap<K, Preferences<K>>,
-    right_prefs: &HashMap<K, Preferences<K>>,
-) -> Result<HashMap<K, K>, &'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<K> = graph.left_partition.keys().cloned().collect();
-    let mut next_proposal: HashMap<K, usize> = HashMap::new();
-    let mut matches: HashMap<K, K> = 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<AgentId, Proposer, Acceptor>,
+) -> Result<(), &'static str> {
+    // Initialize state: all proposers are free
+    let mut free_left: HashSet<AgentId> = graph.left_partition.keys().cloned().collect();
+    let mut proposers = graph.left_partition.clone();
+
+    // Track current engagements: acceptor_id -> proposer_id
+    let mut matches: HashMap<AgentId, AgentId> = 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, &current_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<AgentId, Proposer, Acceptor>) -> bool {
+    // Build a map of current matches from edges
+    let matched_proposers: HashMap<AgentId, AgentId> = matching
+        .edges()
+        .iter()
+        .map(|(p_id, a_id)| (p_id.clone(), a_id.clone()))
+        .collect();
+
+    let matched_acceptors: HashMap<AgentId, AgentId> = 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<Person, Person> 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<u32, Preferences<u32>> = HashMap::from([
-            (1, Preferences::new(vec![2, 3])),
-            (4, Preferences::new(vec![3, 2])),
-        ]);
-
-        let right_prefs: HashMap<u32, Preferences<u32>> = 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<u32, Person, Person> =
+        // 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<AgentId, Proposer> = input
+            .proposers
+            .iter()
+            .cloned()
+            .map(|p| (p.agent.id.clone(), p))
+            .collect();
+
+        let right_partition: HashMap<AgentId, Acceptor> = input
+            .acceptors
+            .iter()
+            .cloned()
+            .map(|a| (a.agent.id.clone(), a))
+            .collect();
+
+        let mut graph: BipartiteGraph<AgentId, Proposer, Acceptor> =
             BipartiteGraph::new(left_partition, right_partition);
 
-        // Run stable matching algorithm
-        let result = stable_match::<u32, Person, Person>(&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<String, Preferences<String>> = 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<String, Preferences<String>> = 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<String, Person, Person> =
+    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<AgentId, Proposer> = input
+            .proposers
+            .iter()
+            .cloned()
+            .map(|p| (p.agent.id.clone(), p))
+            .collect();
+
+        let right_partition: HashMap<AgentId, Acceptor> = input
+            .acceptors
+            .iter()
+            .cloned()
+            .map(|a| (a.agent.id.clone(), a))
+            .collect();
+
+        let mut graph: BipartiteGraph<AgentId, Proposer, Acceptor> =
             BipartiteGraph::new(left_partition, right_partition);
 
-        let result = stable_match::<String, Person, Person>(&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<AgentId, Proposer> = input
+            .proposers
+            .iter()
+            .cloned()
+            .map(|p| (p.agent.id.clone(), p))
+            .collect();
+
+        let right_partition: HashMap<AgentId, Acceptor> = input
+            .acceptors
+            .iter()
+            .cloned()
+            .map(|a| (a.agent.id.clone(), a))
+            .collect();
+
+        let mut graph: BipartiteGraph<AgentId, Proposer, Acceptor> =
+            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");
+        }
     }
 }
index 67c18a4bc85bb8a3e4e6067b2ef66db0e2912eb4..5899a270e8df61faff021e824dd501998938dfc8 100644 (file)
@@ -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<bool> {
+        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<Vec<usize>>, // indices into acceptor_ids
         acceptor_prefs: Vec<Vec<usize>>, // 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<AgentId> = prefs
-                    .iter()
-                    .map(|&idx| acceptor_ids[idx].clone())
-                    .collect();
+                let pref_agents: Vec<AgentId> =
+                    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<AgentId> = prefs
-                    .iter()
-                    .map(|&idx| acceptor_ids[idx].clone())
-                    .collect();
+                let pref_agents: Vec<AgentId> =
+                    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<StableMatchingGraph, &'static str> {
+    use crate::bipartite::{BipartiteGraph, stable_match};
+    use std::collections::HashMap;
+
+    let left_partition: HashMap<AgentId, Proposer> = input
+        .proposers
+        .iter()
+        .cloned()
+        .map(|p| (p.agent.id.clone(), p))
+        .collect();
+
+    let right_partition: HashMap<AgentId, Acceptor> = 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<AgentId> = (0..pref_count)
-                .map(|_| AgentId::arbitrary(g))
-                .collect();
+            let preferences: Vec<AgentId> =
+                (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<AgentId> = (0..pref_count)
-                .map(|_| AgentId::arbitrary(g))
-                .collect();
+            let preferences: Vec<AgentId> =
+                (0..pref_count).map(|_| AgentId::arbitrary(g)).collect();
 
             Acceptor::new(id, preferences)
         }
@@ -208,7 +276,7 @@ mod tests {
                 .collect();
 
             let proposer_prefs: Vec<Vec<usize>> = (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
+    }
 }
index 5f3ad2b90dc042a05f4db049d779f67f027a02cd..00dfcd2eb2f8f19323062b72182b9a04670f2506 100644 (file)
@@ -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 =====