+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> {
/// - `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,
.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)
.entry(right_id)
.or_insert_with(Vec::new)
.push(left_id);
-
Ok(())
}
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, ¤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<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");
+ }
}
}
+use crate::StableMatchingGraph;
use std::collections::HashMap;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
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 {
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();
.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();
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)
.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
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)
}
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)
}
.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)
#[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
}
// (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
+ }
}