-//! # Gale-Shapley Stable Matching Algorithm
-//!
-//! This crate implements the Gale-Shapley algorithm for solving the stable matching problem
-//! using functional programming principles inspired by category theory and type theory.
-//!
-//! The implementation features:
-//! - Type-safe preference handling with validation
-//! - Monadic state management for algorithm execution
-//! - Pure functional composition where possible
-//! - Comprehensive error handling with `Result` types
-//!
-//! ## Example
-//!
-//! ```
-//! use std::collections::HashMap;
-//! use algorithms::stable_matching::*;
-//!
-//! // Generate a random instance and solve it
-//! let problem = generate_random_instance(3)?;
-//! let solution = solve_stable_matching(problem);
-//!
-//! // All men should be matched
-//! assert_eq!(solution.free_men.len(), 0);
-//! # Ok::<(), &'static str>(())
-//! ```
-
-use crate::algorithms::stable_matching::types::{Gender, Person, Preferences};
-use rand::seq::SliceRandom;
-use std::collections::{HashMap, HashSet};
-
-/// The main structure representing a stable matching problem instance.
-///
-/// This contains both the problem specification (people and their preferences)
-/// and the mutable algorithm state (current engagements, proposal history, etc.).
-///
-/// The structure follows functional programming principles by clearly separating
-/// immutable problem data from mutable algorithm state.
-///
-/// ## Algorithm State
-/// The Gale-Shapley algorithm maintains several pieces of state:
-/// - Current engagements between people
-/// - History of proposals made by each person
-/// - Set of currently unmatched people
-///
-/// ## Type Safety
-/// All operations on this structure return `Result` types to handle
-/// error conditions gracefully without panics.
-#[derive(Debug, Clone)]
-pub struct StableMatchingProblem {
- /// All male participants in the matching
- pub men: Vec<Person>,
- /// All female participants in the matching
- pub women: Vec<Person>,
- /// Preference lists for all participants, indexed by person ID
- pub preferences: HashMap<u32, Preferences>,
- // Mutable algorithm state
- /// Current engagements: woman_id -> man_id
- pub engagements: HashMap<u32, u32>,
- /// Proposal history: man_id -> Set<woman_id> of women proposed to
- pub proposal_history: HashMap<u32, HashSet<u32>>,
- /// Set of currently unmatched men
- pub free_men: HashSet<u32>,
-}
+use std::collections::HashMap;
-impl StableMatchingProblem {
- /// Creates a new stable matching problem instance with validation.
- ///
- /// # Arguments
- /// * `men` - Vector of male participants
- /// * `women` - Vector of female participants
- /// * `preferences` - HashMap of preference lists for all participants
- ///
- /// # Returns
- /// * `Ok(StableMatchingProblem)` - Valid problem instance
- /// * `Err(&'static str)` - Error message if validation fails
- ///
- /// # Errors
- /// * Returns error if any man doesn't have Male gender
- /// * Returns error if any woman doesn't have Female gender
- /// * Returns error if the number of men and women are not equal
- ///
- /// # Examples
- ///
- /// ```
- /// # use algorithms::stable_matching::*;
- /// # use std::collections::HashMap;
- /// # use crate::{Person, Gender, Preferences, StableMatchingProblem};
- /// let men = vec![Person { id: 1, gender: Gender::Male }];
- /// let women = vec![Person { id: 2, gender: Gender::Female }];
- /// let mut prefs = HashMap::new();
- /// prefs.insert(1, Preferences::new(1, vec!)?);[11]
- /// prefs.insert(2, Preferences::new(2, vec!)?);[12]
- ///
- /// let problem = StableMatchingProblem::new(men, women, prefs)?;
- /// assert_eq!(problem.free_men.len(), 1);
- /// # Ok::<(), &'static str>(())
- /// ```
- pub fn new(
- men: Vec<Person>,
- women: Vec<Person>,
- preferences: HashMap<u32, Preferences>,
- ) -> Result<Self, &'static str> {
- // Validation
- if men.iter().any(|p| p.gender != Gender::Male) {
- return Err("All men must have Male gender");
- }
- if women.iter().any(|p| p.gender != Gender::Female) {
- return Err("All women must have Female gender");
- }
- if men.len() != women.len() {
- return Err("Must have equal numbers of men and women");
- }
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct AgentId(pub String);
- // Initialize mutable state
- let mut free_men = HashSet::new();
- let mut proposal_history = HashMap::new();
+pub type PreferenceList = Vec<AgentId>;
- for man in &men {
- free_men.insert(man.id);
- proposal_history.insert(man.id, HashSet::new());
- }
-
- Ok(StableMatchingProblem {
- men,
- women,
- preferences,
- engagements: HashMap::new(),
- proposal_history,
- free_men,
- })
- }
+#[derive(Clone, Debug)]
+pub struct Agent {
+ pub id: AgentId,
+}
- /// Retrieves preference list for a given person.
- ///
- /// # Arguments
- /// * `person_id` - The ID of the person whose preferences to retrieve
- ///
- /// # Returns
- /// * `Ok(&Preferences)` - Reference to the person's preferences
- /// * `Err(&'static str)` - Error if no preferences found for this person
- pub fn get_preferences(&self, person_id: u32) -> Result<&Preferences, &'static str> {
- self.preferences
- .get(&person_id)
- .ok_or("No preferences found for person")
- }
+#[derive(Clone, Debug)]
+pub struct Proposer {
+ pub agent: Agent,
+ preferences: PreferenceList,
+ next_to_propose: usize, // private index
+}
- /// Checks if a woman is currently unengaged.
- ///
- /// # Arguments
- /// * `woman_id` - The ID of the woman to check
- ///
- /// # Returns
- /// `true` if the woman is free, `false` if she is engaged
- pub fn is_woman_free(&self, woman_id: u32) -> bool {
- !self.engagements.contains_key(&woman_id)
- }
+#[derive(Clone, Debug)]
+pub struct Acceptor {
+ pub agent: Agent,
+ preferences: PreferenceList,
+ rankings: HashMap<AgentId, usize>, // preferred -> rank (lower = better)
+}
- /// Checks if a man is currently unengaged.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man to check
- ///
- /// # Returns
- /// `true` if the man is free, `false` if he is engaged
- pub fn is_man_free(&self, man_id: u32) -> bool {
- self.free_men.contains(&man_id)
+impl Proposer {
+ pub fn new(id: AgentId, preferences: Vec<AgentId>) -> Self {
+ Self {
+ agent: Agent { id },
+ preferences,
+ next_to_propose: 0,
+ }
}
- /// Gets the ID of the man currently engaged to a woman.
- ///
- /// # Arguments
- /// * `woman_id` - The ID of the woman
- ///
- /// # Returns
- /// * `Some(man_id)` - If the woman is engaged
- /// * `None` - If the woman is free
- pub fn get_engaged_man(&self, woman_id: u32) -> Option<u32> {
- self.engagements.get(&woman_id).copied()
+ pub fn next_proposal(&mut self) -> Option<AgentId> {
+ let proposal = self.preferences.get(self.next_to_propose)?;
+ self.next_to_propose += 1;
+ Some(proposal.clone())
}
+}
- /// Creates an engagement between a man and woman, breaking any existing engagement.
- ///
- /// This operation maintains the invariant that each woman is engaged to at most
- /// one man, and each man is engaged to at most one woman.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man to engage
- /// * `woman_id` - The ID of the woman to engage
- ///
- /// # Side Effects
- /// * If the woman was previously engaged, her former partner becomes free
- /// * The man is removed from the free men set
- /// * The engagement is recorded in the engagements map
- pub fn engage(&mut self, man_id: u32, woman_id: u32) {
- // Break existing engagement if any
- if let Some(current_man) = self.engagements.get(&woman_id) {
- self.free_men.insert(*current_man);
+impl Acceptor {
+ pub fn new(id: AgentId, preferences: Vec<AgentId>) -> Self {
+ let rankings = preferences
+ .iter()
+ .enumerate()
+ .map(|(rank, agent)| (agent.clone(), rank))
+ .collect();
+ Self {
+ agent: Agent { id },
+ preferences,
+ rankings,
}
-
- // Create new engagement
- self.engagements.insert(woman_id, man_id);
- self.free_men.remove(&man_id);
- }
-
- /// Checks if a man has already proposed to a specific woman.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man
- /// * `woman_id` - The ID of the woman
- ///
- /// # Returns
- /// `true` if the man has previously proposed to this woman
- pub fn has_proposed_to(&self, man_id: u32, woman_id: u32) -> bool {
- self.proposal_history
- .get(&man_id)
- .map_or(false, |set| set.contains(&woman_id))
}
- /// Records that a man has proposed to a woman.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man making the proposal
- /// * `woman_id` - The ID of the woman receiving the proposal
- pub fn record_proposal(&mut self, man_id: u32, woman_id: u32) {
- self.proposal_history
- .entry(man_id)
- .or_insert_with(HashSet::new)
- .insert(woman_id);
+ pub fn prefers_over_current(&self, current: Option<&AgentId>, new: &AgentId) -> bool {
+ let new_rank = self.rankings.get(new);
+ match current {
+ None => true,
+ Some(current) => {
+ let current_rank = self.rankings.get(current);
+ new_rank < current_rank
+ }
+ }
}
+}
- /// Checks if a man has proposed to all possible partners.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man to check
- ///
- /// # Returns
- /// `true` if the man has exhausted all possible proposals
- pub fn has_proposed_to_all(&self, man_id: u32) -> bool {
- self.proposal_history
- .get(&man_id)
- .map_or(false, |set| set.len() == self.women.len())
- }
+#[derive(Clone, Debug)]
+pub struct StableMatchingInput {
+ pub proposers: Vec<Proposer>,
+ pub acceptors: Vec<Acceptor>,
+}
- /// Finds the next woman this man should propose to according to his preferences.
- ///
- /// Returns the highest-ranked woman in his preference list to whom he
- /// has not yet proposed.
- ///
- /// # Arguments
- /// * `man_id` - The ID of the man
- ///
- /// # Returns
- /// * `Ok(Some(woman_id))` - The next woman to propose to
- /// * `Ok(None)` - If he has proposed to everyone
- /// * `Err(&'static str)` - If preferences not found for this man
- pub fn next_woman_to_propose(&self, man_id: u32) -> Result<Option<u32>, &'static str> {
- let prefs = self.get_preferences(man_id)?;
- let proposed_set = self.proposal_history.get(&man_id).unwrap();
-
- Ok(prefs
- .ordered_ids
+impl StableMatchingInput {
+ pub fn new(
+ proposer_ids: Vec<AgentId>,
+ acceptor_ids: Vec<AgentId>,
+ proposer_prefs: Vec<Vec<usize>>, // indices into acceptor_ids
+ acceptor_prefs: Vec<Vec<usize>>, // indices into proposer_ids
+ ) -> Self {
+ 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();
+ Acceptor::new(id.clone(), pref_agents)
+ })
+ .collect();
+
+ let proposers = proposer_ids
.iter()
- .find(|&&woman_id| !proposed_set.contains(&woman_id))
- .copied())
+ .zip(proposer_prefs)
+ .map(|(id, prefs)| {
+ let pref_agents: Vec<AgentId> = prefs
+ .iter()
+ .map(|&idx| acceptor_ids[idx].clone())
+ .collect();
+ Proposer::new(id.clone(), pref_agents)
+ })
+ .collect();
+
+ Self { proposers, acceptors }
}
}
-/// A State monad implementation for managing algorithm state transformations.
-///
-/// This follows category theory principles by encapsulating stateful computations
-/// in a composable, purely functional way. The State monad allows us to thread
-/// mutable state through a series of computations while maintaining referential
-/// transparency at the functional level.
-///
-/// ## Category Theory Background
-/// The State monad is defined as `State s a = s -> (a, s)`, representing
-/// a function that takes an initial state and returns a value along with
-/// a new state. This forms a monad with the following operations:
-/// - `return`: Creates a state computation that returns a value without changing state
-/// - `bind` (>>=): Composes state computations sequentially
-///
-/// ## Type Parameters
-/// * `S` - The type of the state being threaded through computations
-/// * `A` - The type of the value produced by the computation
-pub struct State<S, A> {
- run: Box<dyn FnOnce(S) -> (A, S)>,
-}
-impl<S: 'static, A: 'static> State<S, A> {
- /// Creates a new state computation from a function.
- ///
- /// This is the fundamental constructor for the State monad.
- ///
- /// # Arguments
- /// * `f` - A function that takes initial state and returns (value, new_state)
- ///
- /// # Examples
- ///
- /// ```
- /// # use algorithms::stable_matching::*;
- /// // A state computation that increments a counter and returns the old value
- /// let increment = State::new(|count: i32| (count, count + 1));
- /// let (old_value, new_count) = increment.run_state(5);
- /// assert_eq!(old_value, 5);
- /// assert_eq!(new_count, 6);
- /// ```
- pub fn new<F>(f: F) -> Self
- where
- F: FnOnce(S) -> (A, S) + 'static,
- {
- State { run: Box::new(f) }
- }
+pub fn gale_shapley(input: &StableMatchingInput) -> Vec<(AgentId, AgentId)> {
+ let mut proposers = input.proposers.clone();
+ let acceptors = input.acceptors.clone();
- /// Maps a function over the value produced by a state computation.
- ///
- /// This implements the Functor instance for State, allowing transformation
- /// of the computed value without affecting the state threading.
- ///
- /// # Type Parameters
- /// * `B` - The type of the transformed value
- /// * `F` - The transformation function type
- ///
- /// # Arguments
- /// * `f` - Function to transform the computed value
- ///
- /// # Category Theory Note
- /// This satisfies the functor laws:
- /// - `fmap id = id`
- /// - `fmap (g . f) = fmap g . fmap f`
- pub fn map<B: 'static, F: FnOnce(A) -> B + 'static>(self, f: F) -> State<S, B> {
- State::new(move |s| {
- let (a, s1) = (self.run)(s);
- (f(a), s1)
- })
- }
+ let mut engagements: HashMap<AgentId, AgentId> = HashMap::new();
+ let mut free_proposers: Vec<usize> = (0..proposers.len()).collect();
- /// Sequentially composes two state computations.
- ///
- /// This implements the monadic bind operation (>>=), enabling sequential
- /// composition of stateful computations where the result of the first
- /// computation can influence the second.
- ///
- /// # Type Parameters
- /// * `B` - The type of value produced by the second computation
- /// * `F` - The function that creates the second computation
- ///
- /// # Arguments
- /// * `f` - Function that takes the first result and produces a new state computation
- ///
- /// # Monad Laws
- /// This satisfies the monad laws:
- /// - Left identity: `return a >>= f ≡ f a`
- /// - Right identity: `m >>= return ≡ m`
- /// - Associativity: `(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)`
- pub fn flat_map<B: 'static, F: FnOnce(A) -> State<S, B> + 'static>(self, f: F) -> State<S, B> {
- State::new(move |s| {
- let (a, s1) = (self.run)(s);
- (f(a).run)(s1)
- })
- }
+ while let Some(proposer_idx) = free_proposers.pop() {
+ let proposer = &mut proposers[proposer_idx];
- /// Executes the state computation with an initial state.
- ///
- /// This "runs" the state monad computation, providing the initial state
- /// and extracting both the computed value and final state.
- ///
- /// # Arguments
- /// * `initial_state` - The starting state for the computation
- ///
- /// # Returns
- /// A tuple of (computed_value, final_state)
- pub fn run_state(self, initial_state: S) -> (A, S) {
- (self.run)(initial_state)
+ if let Some(acceptor_id) = proposer.next_proposal() {
+ let acceptor_idx = acceptors
+ .iter()
+ .position(|a| a.agent.id == acceptor_id)
+ .expect("StableMatchingInput invariant violated");
+
+ let acceptor = &acceptors[acceptor_idx];
+ 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()) {
+ let old_partner_idx = proposers
+ .iter()
+ .position(|p| p.agent.id == old_partner_id)
+ .expect("Proposer not found");
+ free_proposers.push(old_partner_idx);
+ }
+ } else {
+ free_proposers.push(proposer_idx);
+ }
+ }
}
-}
-/// Pure function to get the next free man from the problem state.
-///
-/// Following functional programming principles, this function has no side effects
-/// and always returns the same output for the same input.
-///
-/// # Arguments
-/// * `state` - Current problem state
-///
-/// # Returns
-/// * `Some(man_id)` - ID of a free man, if any exist
-/// * `None` - If all men are engaged
-fn get_next_free_man(state: &StableMatchingProblem) -> Option<u32> {
- state.free_men.iter().copied().next()
+ engagements
+ .into_iter()
+ .map(|(acceptor_id, proposer_id)| (proposer_id, acceptor_id))
+ .collect()
}
-/// Pure function to find the next woman a man should propose to.
-///
-/// This implements the core logic of the Gale-Shapley algorithm: each man
-/// proposes to women in order of his preference, skipping those he has
-/// already proposed to.
-///
-/// # Arguments
-/// * `state` - Current problem state
-/// * `man_id` - ID of the man making proposals
-///
-/// # Returns
-/// * `Some(woman_id)` - Next woman to propose to
-/// * `None` - If he has proposed to all women
-fn find_next_proposal_target(state: &StableMatchingProblem, man_id: u32) -> Option<u32> {
- state.preferences.get(&man_id).and_then(|prefs| {
- let proposed_set = state.proposal_history.get(&man_id)?;
- prefs
- .ordered_ids
- .iter()
- .find(|&&woman_id| !proposed_set.contains(&woman_id))
- .copied()
- })
-}
-/// Pure function to determine if a woman prefers a new man over her current partner.
-///
-/// This implements the stability condition check: a woman will switch partners
-/// if she prefers the new proposer over her current partner.
-///
-/// # Arguments
-/// * `state` - Current problem state
-/// * `woman_id` - ID of the woman making the choice
-/// * `new_man_id` - ID of the new proposer
-/// * `current_man_id` - ID of her current partner
-///
-/// # Returns
-/// `true` if the woman prefers the new man, `false` otherwise
-///
-/// # Stability Theory
-/// This function implements the core stability check. A matching is stable
-/// if no woman would prefer to switch from her current partner to any man
-/// who would also prefer to switch to her.
-fn woman_prefers_new_man(
- state: &StableMatchingProblem,
- woman_id: u32,
- new_man_id: u32,
- current_man_id: u32,
-) -> bool {
- state
- .preferences
- .get(&woman_id)
- .map(|woman_prefs| {
- let new_pos = woman_prefs
- .ordered_ids
- .iter()
- .position(|&id| id == new_man_id);
- let current_pos = woman_prefs
- .ordered_ids
- .iter()
- .position(|&id| id == current_man_id);
- match (new_pos, current_pos) {
- (Some(new_p), Some(current_p)) => new_p < current_p,
- _ => false,
- }
- })
- .unwrap_or(false)
-}
-
-/// Creates a single state transition for the Gale-Shapley algorithm.
-///
-/// This function encapsulates one iteration of the algorithm:
-/// 1. Find a free man
-/// 2. Find his next preferred woman to propose to
-/// 3. Handle the proposal (engage, reject, or switch partners)
-///
-/// The function is pure in the sense that it returns a state computation
-/// rather than directly mutating state.
-///
-/// # Returns
-/// A State monad computation that performs one algorithm step
-///
-/// # Algorithm Correctness
-/// Each step maintains the algorithm invariants:
-/// - Each woman is engaged to at most one man
-/// - Men propose in preference order
-/// - Women always accept better proposals
-fn update_state() -> State<StableMatchingProblem, ()> {
- State::new(|mut state: StableMatchingProblem| {
- if let Some(man_id) = get_next_free_man(&state) {
- if let Some(woman_id) = find_next_proposal_target(&state, man_id) {
- // Record the proposal
- state
- .proposal_history
- .get_mut(&man_id)
- .unwrap()
- .insert(woman_id);
-
- match state.engagements.get(&woman_id) {
- None => {
- // Woman is free, engage
- state.engagements.insert(woman_id, man_id);
- state.free_men.remove(&man_id);
- }
- Some(¤t_man_id) => {
- // Check if woman prefers new man
- if woman_prefers_new_man(&state, woman_id, man_id, current_man_id) {
- // Switch engagements
- state.engagements.insert(woman_id, man_id);
- state.free_men.remove(&man_id);
- state.free_men.insert(current_man_id);
- }
- // Otherwise, man remains free
- }
- }
- }
+#[cfg(test)]
+mod tests {
+ use quickcheck_macros::quickcheck;
+ use crate::{Acceptor, AgentId, Proposer, gale_shapley, StableMatchingInput};
+ use quickcheck::{Arbitrary, Gen};
+ impl Arbitrary for AgentId {
+ fn arbitrary(g: &mut Gen) -> Self {
+ // Generate a simple ID from a number
+ let num = u32::arbitrary(g);
+ AgentId(format!("agent_{}", num))
}
- ((), state)
- })
-}
+ }
-/// Solves the stable matching problem using functional composition.
-///
-/// This function repeatedly applies the state transformation until no free men remain,
-/// implementing the complete Gale-Shapley algorithm through monadic composition.
-///
-/// # Arguments
-/// * `problem` - Initial problem instance with all men free
-///
-/// # Returns
-/// The solved problem with all participants matched
-///
-/// # Algorithm Properties
-/// The Gale-Shapley algorithm guarantees:
-/// - **Termination**: The algorithm always terminates in O(n²) proposals
-/// - **Stability**: The resulting matching is stable (no blocking pairs)
-/// - **Optimality**: The matching is man-optimal and woman-pessimal
-///
-/// # Examples
-///
-/// ```
-/// # use algorithms::stable_matching::*;
-/// let problem = generate_random_instance(4)?;
-/// let solution = solve_stable_matching(problem);
-///
-/// // Verify all men are matched
-/// assert_eq!(solution.free_men.len(), 0);
-/// assert_eq!(solution.engagements.len(), 4);
-/// # Ok::<(), &'static str>(())
-/// ```
-//pub fn solve_stable_matching(mut problem: StableMatchingProblem) -> StableMatchingProblem {
-// while !problem.free_men.is_empty() {
-// let (_, new_state) = update_state().run_state(problem);
-// problem = new_state;
-// }
-// problem
-//}
-
-/// Solves the stable matching problem using functional unfold composition.
-///
-/// This implementation uses `std::iter::successors` to express the algorithm
-/// as an unfold (anamorphism) - generating successive problem states from
-/// the initial configuration until convergence to a stable matching.
-///
-/// # Arguments
-/// * `problem` - Initial problem instance with all men free
-///
-/// # Returns
-/// The solved problem with all participants matched
-///
-/// # Algorithm Complexity
-/// - Time: O(n²) in worst case
-/// - Space: O(n) for state representation
-///
-/// # Functional Programming Note
-/// This demonstrates the unfold pattern - the categorical dual of fold.
-/// While fold consumes structure to produce values, unfold generates
-/// structure from an initial seed value.
-///
-/// # Examples
-///
-/// ```
-/// # use algorithms::stable_matching::*;
-/// let problem = generate_random_instance(4)?;
-/// let solution = solve_stable_matching(problem);
-///
-/// assert_eq!(solution.free_men.len(), 0);
-/// assert_eq!(solution.engagements.len(), 4);
-/// # Ok::<(), &'static str>(())
-/// ```
-pub fn solve_stable_matching(problem: StableMatchingProblem) -> StableMatchingProblem {
- std::iter::successors(Some(problem), |current_problem| {
- if current_problem.free_men.is_empty() {
- None // Algorithm has converged - no more states to generate
- } else {
- // Generate next state using monadic state transformation
- let (_, next_state) = update_state().run_state(current_problem.clone());
- Some(next_state)
- }
- })
- .last() // Extract the final converged state
- .expect("Iterator should always yield at least the initial state")
-}
+ impl Arbitrary for Proposer {
+ fn arbitrary(g: &mut Gen) -> Self {
+ 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();
-/// Generates a random stable matching problem instance for testing and experimentation.
-///
-/// This function creates a problem with the specified number of men and women,
-/// where each person has a random preference ordering over all potential partners.
-///
-/// # Arguments
-/// * `count` - Number of men and women to create (total 2×count participants)
-///
-/// # Returns
-/// * `Ok(StableMatchingProblem)` - Random problem instance ready to solve
-/// * `Err(&'static str)` - Error if problem creation fails
-///
-/// # Randomization
-/// Uses Fisher-Yates shuffle to create uniformly random preference orderings,
-/// ensuring each possible preference profile has equal probability.
-///
-/// # Examples
-///
-/// ```
-/// # use algorithms::stable_matching::*;
-/// // Create a problem with 5 men and 5 women
-/// let problem = generate_random_instance(5)?;
-///
-/// assert_eq!(problem.men.len(), 5);
-/// assert_eq!(problem.women.len(), 5);
-/// assert_eq!(problem.preferences.len(), 10); // 5 + 5
-/// # Ok::<(), &'static str>(())
-/// ```
-///
-/// # Use Cases
-/// - **Testing**: Generate test cases for algorithm verification
-/// - **Benchmarking**: Create instances for performance analysis
-/// - **Research**: Study algorithm behavior on random instances
-pub fn generate_random_instance(count: usize) -> Result<StableMatchingProblem, &'static str> {
- let mut rng = rand::thread_rng();
-
- // Create people using functional combinators
- let men: Vec<Person> = (1..=count as u32)
- .map(|id| Person {
- id,
- gender: Gender::Male,
- })
- .collect();
-
- let women: Vec<Person> = (1..=count as u32)
- .map(|id| Person {
- id,
- gender: Gender::Female,
- })
- .collect();
-
- let mut preferences = HashMap::new();
-
- // Generate men's preferences functionally
- for man in &men {
- let mut women_ids: Vec<u32> = (1..=count as u32).collect();
- women_ids.shuffle(&mut rng);
-
- let prefs = Preferences::new(man.id, women_ids)?;
- preferences.insert(man.id, prefs);
+ Proposer::new(id, preferences)
+ }
}
- // Generate women's preferences functionally
- for woman in &women {
- let mut men_ids: Vec<u32> = (1..=count as u32).collect();
- men_ids.shuffle(&mut rng);
+ impl Arbitrary for Acceptor {
+ fn arbitrary(g: &mut Gen) -> Self {
+ 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 prefs = Preferences::new(woman.id, men_ids)?;
- preferences.insert(woman.id, prefs);
+ Acceptor::new(id, preferences)
+ }
}
- StableMatchingProblem::new(men, women, preferences)
-}
+ impl Arbitrary for StableMatchingInput {
+ fn arbitrary(g: &mut Gen) -> Self {
+ let num_agents = usize::arbitrary(g).min(10).max(2);
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_preferences_creation() {
- let prefs = Preferences::new(1, vec![2, 3, 4]).unwrap();
- assert_eq!(prefs.most_preferred(), 2);
- assert!(prefs.prefers(2, 3).unwrap());
- assert!(!prefs.prefers(4, 2).unwrap());
- }
+ // Generate acceptor IDs first
+ let acceptor_ids: Vec<AgentId> = (0..num_agents)
+ .map(|i| AgentId(format!("A{}", i)))
+ .collect();
- #[test]
- fn test_problem_creation() {
- let men = vec![
- Person {
- id: 1,
- gender: Gender::Male,
- },
- Person {
- id: 2,
- gender: Gender::Male,
- },
- ];
- let women = vec![
- Person {
- id: 1,
- gender: Gender::Female,
- },
- Person {
- id: 2,
- gender: Gender::Female,
- },
- ];
-
- let mut preferences = HashMap::new();
- preferences.insert(1, Preferences::new(1, vec![1, 2]).unwrap()); // Man 1 prefs
- preferences.insert(2, Preferences::new(2, vec![2, 1]).unwrap()); // Man 2 prefs
-
- let problem = StableMatchingProblem::new(men, women, preferences).unwrap();
- assert_eq!(problem.free_men.len(), 2);
- }
+ // Generate proposer IDs
+ let proposer_ids: Vec<AgentId> = (0..num_agents)
+ .map(|i| AgentId(format!("P{}", i)))
+ .collect();
- #[test]
- fn test_generate_random_instance() {
- // Generate a small test instance (e.g., 4 men and 4 women)
- let n = 4;
- let result = generate_random_instance(n);
-
- assert!(
- result.is_ok(),
- "generate_random_instance returned an error: {:?}",
- result.err()
- );
-
- let instance = result.unwrap();
-
- // Check men and women count
- assert_eq!(
- instance.men.len(),
- n,
- "Incorrect number of men generated [attached_file:1]"
- );
- assert_eq!(
- instance.women.len(),
- n,
- "Incorrect number of women generated [attached_file:1]"
- );
-
- // Check that preferences exist for each man and woman
- for man in &instance.men {
- assert!(
- instance.preferences.contains_key(&man.id),
- "Preference list missing for man {} [attached_file:1]",
- man.id
- );
- let prefs = instance.preferences.get(&man.id).unwrap();
- // Preferences should list all women without duplicates
- assert_eq!(
- prefs.ordered_ids.len(),
- n,
- "Man {} preferences should have length {} [attached_file:1]",
- man.id,
- n
- );
- let mut sorted = prefs.ordered_ids.clone();
- sorted.sort();
- sorted.dedup();
- assert_eq!(
- sorted.len(),
- n,
- "Man {} preferences contain duplicates [attached_file:1]",
- man.id
- );
- }
+ // Generate random permutations for preferences
+ let acceptor_prefs: Vec<Vec<usize>> = (0..num_agents)
+ .map(|_| generate_permutation(g, num_agents))
+ .collect();
- for woman in &instance.women {
- assert!(
- instance.preferences.contains_key(&woman.id),
- "Preference list missing for woman {} [attached_file:1]",
- woman.id
- );
- let prefs = instance.preferences.get(&woman.id).unwrap();
- assert_eq!(
- prefs.ordered_ids.len(),
- n,
- "Woman {} preferences should have length {} [attached_file:1]",
- woman.id,
- n
- );
- let mut sorted = prefs.ordered_ids.clone();
- sorted.sort();
- sorted.dedup();
- assert_eq!(
- sorted.len(),
- n,
- "Woman {} preferences contain duplicates [attached_file:1]",
- woman.id
- );
- }
+ let proposer_prefs: Vec<Vec<usize>> = (0..num_agents)
+ .map(|_| generate_permutation(g, num_agents)) // Full permutation
+ .collect();
- // Ensure free_men are all the men
- for man in &instance.men {
- assert!(
- instance.free_men.contains(&man.id),
- "Man {} should be free initially [attached_file:1]",
- man.id
- );
+ StableMatchingInput::new(proposer_ids, acceptor_ids, proposer_prefs, acceptor_prefs)
}
+ }
- // Ensure proposal_history is initialized for all men and is empty
- for man in &instance.men {
- let history = instance.proposal_history.get(&man.id).unwrap();
- assert!(
- history.is_empty(),
- "Proposal history for man {} should be empty [attached_file:1]",
- man.id
- );
+ fn generate_permutation(g: &mut Gen, n: usize) -> Vec<usize> {
+ let mut perms: Vec<usize> = (0..n).collect();
+ for i in 0..n {
+ let j = i + usize::arbitrary(g) % (n.saturating_sub(i));
+ perms.swap(i, j);
}
+ perms
+ }
+ #[quickcheck]
+ fn prop_proposer_can_propose(mut proposer: Proposer) -> bool {
+ // A proposer should always be able to make at least one proposal
+ proposer.next_proposal().is_some()
}
- /// Test that demonstrates the documented API usage
- #[test]
- fn test_basic_functionality() -> Result<(), &'static str> {
- let problem = generate_random_instance(3)?;
- let solution = solve_stable_matching(problem);
-
- // All men should be matched
- assert_eq!(solution.free_men.len(), 0);
- assert_eq!(solution.engagements.len(), 3);
+ #[quickcheck]
+ fn prop_complete_matching(input: StableMatchingInput) -> bool {
+ let matches = gale_shapley(&input); // Borrow with &
+ let max_possible = input.proposers.len().min(input.acceptors.len());
+ matches.len() == max_possible
+ }
- Ok(())
+ #[quickcheck]
+ fn prop_proposers_satisfied(input: StableMatchingInput) -> bool {
+ let matches = gale_shapley(&input);
+ // In proposer-optimal matching, no proposer prefers an unmatched acceptor
+ // (simplified check - full stability requires more complex logic)
+ true // Placeholder for full stability property
}
}