]> localhost Git - algorithms.git/commitdiff
added bipartite
authorJeff Hemminger <jeff@hemminger.haus>
Fri, 19 Dec 2025 04:48:25 +0000 (13:48 +0900)
committerJeff Hemminger <jeff@hemminger.haus>
Fri, 19 Dec 2025 04:48:25 +0000 (13:48 +0900)
.gitignore
Cargo.lock
Cargo.toml
src/algorithms/interview_scheduling/earliest_completion.rs
src/algorithms/interview_scheduling/mod.rs
src/algorithms/interview_scheduling/weighted_interval_scheduling.rs [new file with mode: 0644]
src/algorithms/stable_matching/bipartite.rs [new file with mode: 0644]
src/algorithms/stable_matching/gale_shapley.rs
src/algorithms/stable_matching/mod.rs
src/algorithms/stable_matching/types.rs [new file with mode: 0644]
src/main.rs [deleted file]

index ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba..2a0038a460f6e43b38eaff116c8517a3a9f8e07c 100644 (file)
@@ -1 +1,2 @@
 /target
+.idea
\ No newline at end of file
index fb81a6325ce1879247fd17063862b44d9d4ae2c2..6ecfbb64187aaaf2085381a2e5561f978a78fb1c 100644 (file)
@@ -6,7 +6,10 @@ version = 4
 name = "algorithms"
 version = "0.1.0"
 dependencies = [
+ "csv",
  "rand",
+ "serde",
+ "serde_json",
 ]
 
 [[package]]
@@ -15,6 +18,27 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
 
+[[package]]
+name = "csv"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.16"
@@ -26,12 +50,24 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
 [[package]]
 name = "libc"
 version = "0.2.175"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
 
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.21"
@@ -89,6 +125,55 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.106"
index 7ca51a74316d8268f29b57ac140107899192a5b1..701c04e81c57bb6f8bdd628a6687a4fdba0d71ee 100644 (file)
@@ -5,6 +5,9 @@ edition = "2024"
 
 [dependencies]
 rand = "0.8"
+csv = "1.3"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
 
 [lib]
 doctest = false
index 3ecda6ae43b559729472cc8fe20ebca82237e82b..53973869145a8ca71bfca6644c2924aad3c4ce35 100644 (file)
@@ -32,8 +32,41 @@ where
         // This is a placeholder
         self.end.0
     }
-}
 
+    /// Sort interviews by end time (earliest completion first)
+    /// This is the optimal greedy algorithm for interval scheduling
+    pub fn sort_by_earliest_completion(interviews: &mut [Interview<T>]) {
+        interviews.sort_by(|a, b| a.end.cmp(&b.end));
+    }
+}    
+
+/// Optimal interval scheduling using earliest completion time
+/// Returns maximum set of non-overlapping interviews
+pub fn schedule_interviews_optimal<T>(mut interviews: Vec<Interview<T>>) -> Vec<Interview<T>>
+where
+    T: Ord + Copy,
+{
+    if interviews.is_empty() {
+        return vec![];
+    }
+    
+    // Step 1: Sort by earliest completion time
+    Interview::sort_by_earliest_completion(&mut interviews);
+    
+    // Step 2: Greedily select non-overlapping intervals
+    let mut selected = vec![interviews[0].clone()]; 
+    
+    for interview in interviews.into_iter().skip(1) {
+        let last_selected = selected.last().unwrap();
+        
+        // If this interview starts after the last one ends, select it
+        if interview.start >= last_selected.end {
+            selected.push(interview);
+        }
+    }
+    
+    selected
+}
 
 #[cfg(test)]
 mod tests {
index 00161637e62dea6cca316d2b613176c5a2dbb076..4dd6544a1fa95f88f3bdd3ca1fe35bea33745b72 100644 (file)
@@ -1,6 +1,8 @@
 
 pub mod earliest_completion;
+pub mod weighted_interval_scheduling;
 pub mod test_data;
 
 pub use earliest_completion::*;
+pub use weighted_interval_scheduling::*;
 pub use test_data::*;
diff --git a/src/algorithms/interview_scheduling/weighted_interval_scheduling.rs b/src/algorithms/interview_scheduling/weighted_interval_scheduling.rs
new file mode 100644 (file)
index 0000000..de1d84b
--- /dev/null
@@ -0,0 +1,445 @@
+
+//! Weighted Interval Scheduling Algorithm using Dynamic Programming
+//! 
+//! This implementation follows the approach described in Algorithm Design by Kleinberg & Tardos
+//! The algorithm finds the maximum weight subset of non-overlapping intervals.
+
+use std::cmp;
+
+/// Represents a job/interval with start time, finish time, and weight/value
+#[derive(Debug, Clone)]
+pub struct Job {
+    pub start: i32,
+    pub finish: i32,
+    pub weight: i32,
+    pub id: usize,  // For tracking original job indices
+}
+
+impl Job {
+    /// Creates a new job with the given parameters
+    pub fn new(start: i32, finish: i32, weight: i32, id: usize) -> Self {
+        Job { start, finish, weight, id }
+    }
+}
+
+/// Weighted Interval Scheduling solver using Dynamic Programming
+pub struct WeightedScheduler {
+    jobs: Vec<Job>,
+    dp: Vec<i32>,           // DP table: dp[i] = maximum weight using first i jobs
+    n: usize,                   // Cache the length    
+    predecessors: Vec<Option<usize>>, // predecessors[i] = latest compatible job index for job i
+}
+
+impl WeightedScheduler {
+
+    /// Gets the DP value for the predecessor of a jobs
+    fn predecessor_value(&self, job_idx: usize) -> i32 {
+        self.predecessors[job_idx]
+            .map_or(0, |pred| self.dp[pred + 1])
+    }    
+    
+    /// Creates a new scheduler with the given jobs
+    pub fn new(mut jobs: Vec<Job>) -> Self {
+        // Sort jobs by finish time (crucial for the DP algorithm)
+        jobs.sort_by(|a, b| a.finish.cmp(&b.finish));
+
+        let n = jobs.len();
+        let dp = vec![0; n + 1]; // dp[0] = 0 (base case: no jobs)
+        let predecessors = vec![None; n];  // Will be computed later
+       
+        WeightedScheduler {
+            jobs,
+            dp,
+            n,     
+            predecessors,
+        }
+    }
+
+    /// Computes the predecessor function p(j) for all jobs
+    /// p(j) = largest index i < j such that job i is compatible with job j
+    /// (i.e., jobs[i].finish <= jobs[j].start)
+    fn compute_predecessors(&mut self) {
+
+        for j in 0..self.n {
+            // Find the latest job that finishes before job j starts
+            // Using binary search for O(log n) performance per job
+            self.predecessors[j] = self.find_latest_compatible(j);
+        }
+    }
+
+    /// Binary search to find the latest compatible job for job at index j
+    fn find_latest_compatible(&self, j: usize) -> Option<usize> {
+        let target_start = self.jobs[j].start;
+        let mut left = 0i32;
+        let mut right = (j as i32) - 1;
+        let mut result = None;
+
+        while left <= right {
+            let mid = left + (right - left) / 2;
+
+            if self.jobs[mid as usize].finish <= target_start {
+                result = Some(mid as usize);  // Changed: Some(mid as usize) instead of mid
+                left = mid + 1;                
+            } else {
+                right = mid - 1;
+            }
+        }
+
+        result
+    }
+
+    /// Main dynamic programming algorithm to find maximum weight
+    /// Returns the maximum total weight achievable
+    pub fn find_max_weight(&mut self) -> i32 {
+        if self.n == 0 {
+            return 0;
+        }
+
+        // Step 1: Compute predecessors for all jobs
+        self.compute_predecessors();
+
+        // Step 2: Fill DP table using the recurrence relation:
+        // dp[i] = max(dp[i-1], weight[i-1] + dp[p(i-1)+1])
+        // where p(i-1) is the predecessor of job i-1
+
+        for i in 1..=self.n {
+            let job_idx = i - 1; // Job index (0-based)
+            let current_weight = self.jobs[job_idx].weight;
+
+            // Option 1: Don't include current job
+            let exclude = self.dp[i - 1];
+
+            // Option 2: Include current job
+            let include = current_weight + self.predecessor_value(job_idx);        
+
+            // Take the maximum of the two options
+            self.dp[i] = cmp::max(exclude, include);
+        }
+
+        self.dp[self.n]
+    }
+
+    /// Reconstructs the actual optimal solution (which jobs to select)
+    /// Returns a vector of job indices in the optimal solution
+    pub fn find_optimal_jobs(&self) -> Vec<usize> {
+        if self.n == 0 {
+            return Vec::new();
+        }
+
+        let mut solution = Vec::new();
+        let mut i = self.n;
+
+        // Backtrack through the DP table to find which jobs were selected
+        while i > 0 {
+            let job_idx = i - 1;
+            let current_weight = self.jobs[job_idx].weight;
+
+            // Check if job i was included in the optimal solution
+            let include_value = current_weight + self.predecessor_value(job_idx);
+
+            if include_value > self.dp[i - 1] {
+                // Job i was included
+                solution.push(self.jobs[job_idx].id);
+
+                // Move to the predecessor             
+                match self.predecessors[job_idx] {
+                    Some(pred) => i = pred + 1,
+                    None => break,
+                }              
+
+            } else {
+                // Job i was not included
+                i -= 1;
+            }
+        }
+
+        // Reverse to get jobs in chronological order
+        solution.reverse();
+        solution
+    }
+
+    /// Solves the weighted interval scheduling problem
+    /// Returns (maximum_weight, selected_job_ids)
+    pub fn solve(&mut self) -> (i32, Vec<usize>) {
+        let max_weight = self.find_max_weight();
+        let selected_jobs = self.find_optimal_jobs();
+        (max_weight, selected_jobs)
+    }
+
+    /// Pretty prints the solution
+    pub fn print_solution(&self, max_weight: i32, selected_jobs: &[usize]) {
+        println!("\n=== Weighted Interval Scheduling Solution ===");
+        println!("Maximum total weight: {}", max_weight);
+        println!("Selected jobs:");
+
+        for &job_id in selected_jobs {
+            // Find the job with this ID
+            if let Some(job) = self.jobs.iter().find(|j| j.id == job_id) {
+                println!("  Job {}: [{}→{}, weight={}]", 
+                         job.id, job.start, job.finish, job.weight);
+            }
+        }
+
+        println!("\nAll jobs (sorted by finish time):");
+        for (_i, job) in self.jobs.iter().enumerate() {
+            let selected = selected_jobs.contains(&job.id);
+            let marker = if selected { "✓" } else { " " };
+            println!("  {} Job {}: [{}→{}, weight={}]", 
+                     marker, job.id, job.start, job.finish, job.weight);
+        }
+    }
+}
+
+/// Example and test function
+fn main() {
+    println!("Weighted Interval Scheduling with Dynamic Programming\n");
+
+    // Example 1: Basic test case
+    println!("=== Example 1: Basic Case ===");
+    let jobs1 = vec![
+        Job::new(1, 4, 3, 1),
+        Job::new(2, 6, 5, 2), 
+        Job::new(4, 7, 2, 3),
+        Job::new(6, 8, 4, 4),
+    ];
+
+    let mut scheduler1 = WeightedScheduler::new(jobs1);
+    let (max_weight1, solution1) = scheduler1.solve();
+    scheduler1.print_solution(max_weight1, &solution1);
+
+    // Example 2: Case where greedy by weight fails
+    println!("\n\n=== Example 2: Greedy by Weight Fails ===");
+    let jobs2 = vec![
+        Job::new(0, 10, 10, 1),  // Long, high-weight job
+        Job::new(1, 2, 3, 2),    // Short jobs that together
+        Job::new(3, 4, 3, 3),    // are better than the
+        Job::new(5, 6, 3, 4),    // single long job
+        Job::new(7, 8, 3, 5),    
+    ];
+
+    let mut scheduler2 = WeightedScheduler::new(jobs2);
+    let (max_weight2, solution2) = scheduler2.solve();
+    scheduler2.print_solution(max_weight2, &solution2);
+
+    // Example 3: Textbook example (if we can infer from description)
+    println!("\n\n=== Example 3: Larger Test Case ===");
+    let jobs3 = vec![
+        Job::new(0, 6, 8, 1),
+        Job::new(1, 4, 2, 2),
+        Job::new(3, 5, 4, 3),
+        Job::new(3, 8, 7, 4),
+        Job::new(4, 7, 1, 5),
+        Job::new(5, 9, 3, 6),
+        Job::new(6, 10, 2, 7),
+        Job::new(8, 11, 4, 8),
+    ];
+
+    let mut scheduler3 = WeightedScheduler::new(jobs3);
+    let (max_weight3, solution3) = scheduler3.solve();
+    scheduler3.print_solution(max_weight3, &solution3);
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // Base test utilities - functional approach with immutable data
+    fn create_jobs_basic() -> Vec<Job> {
+        vec![
+            Job::new(1, 4, 3, 1),
+            Job::new(2, 6, 5, 2), 
+            Job::new(4, 7, 2, 3),
+            Job::new(6, 8, 4, 4),
+        ]
+    }
+
+    fn create_jobs_greedy_fails() -> Vec<Job> {
+        vec![
+            Job::new(0, 10, 10, 1),  // Long, high-weight job
+            Job::new(1, 2, 3, 2),    // Short jobs that together
+            Job::new(3, 4, 3, 3),    // are better than the
+            Job::new(5, 6, 3, 4),    // single long job
+            Job::new(7, 8, 3, 5),    
+        ]
+    }
+
+    fn create_jobs_large_case() -> Vec<Job> {
+        vec![
+            Job::new(0, 6, 8, 1),
+            Job::new(1, 4, 2, 2),
+            Job::new(3, 5, 4, 3),
+            Job::new(3, 8, 7, 4),
+            Job::new(4, 7, 1, 5),
+            Job::new(5, 9, 3, 6),
+            Job::new(6, 10, 2, 7),
+            Job::new(8, 11, 4, 8),
+        ]
+    }
+
+    // Pure function to verify solution validity
+    fn is_valid_solution(jobs: &[Job], solution: &[usize]) -> bool {
+        solution.windows(2).all(|pair| {
+            let job1 = &jobs[pair[0]];
+            let job2 = &jobs[pair[1]];
+            job1.finish <= job2.start
+        })
+    }
+
+    // Pure function to calculate total weight
+    fn calculate_total_weight(jobs: &[Job], solution: &[usize]) -> i32 {
+        solution.iter()
+            .map(|&idx| jobs[idx].weight)
+            .map(|w| i32::try_from(w).expect("Weight overflow"))
+            .sum()     
+    }    
+
+    // Example 1: Basic test case
+    #[test]
+    fn test_basic_case_example() {
+        let jobs = create_jobs_basic();
+        let mut scheduler = WeightedScheduler::new(jobs.clone());
+        let (max_weight, solution) = scheduler.solve();
+        
+        // Verify solution is valid
+        assert!(is_valid_solution(&jobs, &solution));
+        
+        // Verify calculated weight matches
+        assert_eq!(max_weight, calculate_total_weight(&jobs, &solution));
+        
+        // For this specific case, optimal weight should be 9
+        // (jobs with weights 5 and 4, indices 1 and 3)
+        assert_eq!(max_weight, 9);
+    }
+
+    // Example 2: Case where greedy by weight fails
+    #[test]
+    fn test_greedy_by_weight_fails() {
+        let jobs = create_jobs_greedy_fails();
+        let mut scheduler = WeightedScheduler::new(jobs.clone());
+        let (max_weight, solution) = scheduler.solve();
+        
+        // Verify solution validity
+        assert!(is_valid_solution(&jobs, &solution));
+        assert_eq!(max_weight, calculate_total_weight(&jobs, &solution));
+        
+        // The optimal solution should choose the four short jobs (weight 12)
+        // rather than the single long job (weight 10)
+        assert_eq!(max_weight, 12);
+        assert_eq!(solution.len(), 4);
+    }
+
+    // Example 3: Larger test case
+    #[test]  
+    fn test_larger_case_example() {
+        let jobs = create_jobs_large_case();
+        let mut scheduler = WeightedScheduler::new(jobs.clone());
+        let (max_weight, solution) = scheduler.solve();
+        
+        // Verify solution validity
+        assert!(is_valid_solution(&jobs, &solution));
+        assert_eq!(max_weight, calculate_total_weight(&jobs, &solution));
+        
+        // For this case, we just ensure we get a reasonable result
+        assert!(max_weight > 0);
+        assert!(!solution.is_empty());
+    }
+
+    // Property-based test: solution should always be valid
+    #[test]
+    fn test_solution_validity_property() {
+        let test_cases = vec![
+            create_jobs_basic(),
+            create_jobs_greedy_fails(), 
+            create_jobs_large_case(),
+        ];
+        
+        for jobs in test_cases {
+            let mut scheduler = WeightedScheduler::new(jobs.clone());
+            let (max_weight, solution) = scheduler.solve();
+            
+            // Property: solution must be valid (no overlapping jobs)
+            assert!(is_valid_solution(&jobs, &solution));
+            
+            // Property: calculated weight must match returned weight
+            assert_eq!(max_weight, calculate_total_weight(&jobs, &solution));
+        }
+    }
+
+    // Functional testing pattern: test with transformations
+    #[test]
+    fn test_job_order_independence() {
+        let mut jobs = create_jobs_basic();
+        let mut scheduler1 = WeightedScheduler::new(jobs.clone());
+        let (weight1, _) = scheduler1.solve();
+        
+        // Test that reversing job order doesn't change optimal weight
+        jobs.reverse();
+        let mut scheduler2 = WeightedScheduler::new(jobs);
+        let (weight2, _) = scheduler2.solve();
+        
+        assert_eq!(weight1, weight2);
+    }
+
+    // Test edge case: all jobs overlap
+    #[test]
+    fn test_all_jobs_overlap() {
+        let jobs = vec![
+            Job::new(1, 5, 10, 0),
+            Job::new(2, 6, 8, 1), 
+            Job::new(3, 7, 12, 2),
+            Job::new(4, 8, 6, 3),
+        ];
+        
+        let mut scheduler = WeightedScheduler::new(jobs.clone());
+        let (max_weight, solution) = scheduler.solve();
+        
+        // Should pick the highest weight job (12)
+        assert_eq!(max_weight, 12);
+        assert_eq!(solution.len(), 1);
+        assert_eq!(solution[0], 2); // Job with weight 12
+    }
+    
+    #[test]
+    fn test_empty_jobs() {
+        let mut scheduler = WeightedScheduler::new(vec![]);
+        let (max_weight, solution) = scheduler.solve();
+        assert_eq!(max_weight, 0);
+        assert!(solution.is_empty());
+    }
+
+    #[test]
+    fn test_single_job() {
+        let jobs = vec![Job::new(1, 3, 5, 0)];
+        let mut scheduler = WeightedScheduler::new(jobs);
+        let (max_weight, solution) = scheduler.solve();
+        assert_eq!(max_weight, 5);
+        assert_eq!(solution, vec![0]);
+    }
+
+    #[test]
+    fn test_no_overlap() {
+        let jobs = vec![
+            Job::new(1, 2, 3, 0),
+            Job::new(3, 4, 2, 1),
+            Job::new(5, 6, 4, 2),
+        ];
+        let mut scheduler = WeightedScheduler::new(jobs);
+        let (max_weight, solution) = scheduler.solve();
+        assert_eq!(max_weight, 9); // All jobs can be selected
+        assert_eq!(solution.len(), 3);
+    }
+
+    #[test]
+    fn test_basic_overlap() {
+        let jobs = vec![
+            Job::new(1, 4, 3, 0),
+            Job::new(2, 6, 5, 1),
+            Job::new(4, 7, 2, 2),
+            Job::new(6, 8, 4, 3),
+        ];
+        let mut scheduler = WeightedScheduler::new(jobs);
+        let (max_weight, _solution) = scheduler.solve();
+        // Optimal solution should pick jobs 1 (weight 5) and 3 (weight 4) = 9
+        assert_eq!(max_weight, 9);
+    }
+}
diff --git a/src/algorithms/stable_matching/bipartite.rs b/src/algorithms/stable_matching/bipartite.rs
new file mode 100644 (file)
index 0000000..cd65d53
--- /dev/null
@@ -0,0 +1,308 @@
+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> {
+    fn can_connect(&self, left: &L, right: &R) -> bool;
+}
+
+/// Generic bipartite graph structure with independent left and right node types.
+/// Type parameters:
+/// - `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 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> {
+    pub fn new(left_partition: HashMap<K, L>, right_partition: HashMap<K, R>) -> Self {
+        BipartiteGraph {
+            left_partition,
+            right_partition,
+            edges_from_left: HashMap::new(),
+            edges_from_right: HashMap::new(),
+        }
+    }
+
+    pub fn add_edge(&mut self, left_id: K, right_id: K) -> Result<(), &'static str> {
+        self.left_partition
+            .contains_key(&left_id)
+            .then(|| ())
+            .ok_or("Left node ID not found")?;
+        self.right_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)
+            .push(right_id.clone());
+        self.edges_from_right
+            .entry(right_id)
+            .or_insert_with(Vec::new)
+            .push(left_id);
+
+        Ok(())
+    }
+
+    pub fn neighbors_of_left(&self, left_id: &K) -> Option<&[K]> {
+        self.edges_from_left.get(left_id).map(|v| v.as_slice())
+    }
+
+    pub fn neighbors_of_right(&self, right_id: &K) -> Option<&[K]> {
+        self.edges_from_right.get(right_id).map(|v| v.as_slice())
+    }
+}
+
+/// 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
+///
+/// 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;
+        }
+
+        let right_id = prefs.ordered_ids[*proposal_index].clone();
+        *proposal_index += 1;
+
+        // Verify right node exists
+        if !graph.right_partition.contains_key(&right_id) {
+            return Err("Proposed right node does not exist");
+        }
+
+        // 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);
+                }
+                // Else: reject proposal, keep left node free for next iteration
+            }
+        }
+    }
+
+    Ok(matches)
+}
+
+
+// ============================================================================
+// Example Usage and Tests
+// ============================================================================
+
+#[derive(Clone, Debug)]
+pub struct Person {
+    pub id: u32,
+    pub name: String,
+    pub gender: Gender,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum Gender {
+    Male,
+    Female,
+}
+
+pub struct GenderValidator;
+
+impl Compatible<Person, Person> for GenderValidator {
+    fn can_connect(&self, left: &Person, right: &Person) -> bool {
+        left.gender != right.gender
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[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> =
+            BipartiteGraph::new(left_partition, right_partition);
+
+        // Run stable matching algorithm
+        let result = stable_match::<u32, Person, Person>(
+            &graph,
+            &left_prefs,
+            &right_prefs,
+        );
+
+        assert!(result.is_ok());
+        let matches = result.unwrap();
+        assert_eq!(matches.len(), 2); // Both should be matched
+
+        println!("Matches: {:?}", matches);
+    }
+
+    #[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> =
+            BipartiteGraph::new(left_partition, right_partition);
+
+        let result = stable_match::<String, Person, Person>(
+            &graph,
+            &left_prefs,
+            &right_prefs,
+        );
+
+        assert!(result.is_ok());
+        let matches = result.unwrap();
+        assert_eq!(matches.len(), 2);
+        println!("String-keyed matches: {:?}", matches);
+    }
+
+    #[test]
+    fn test_preference_order() {
+        let prefs = Preferences::new(vec![1, 2, 3, 4]);
+
+        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
+    }
+}
index 5d49ce434c8c7dfae9bb9439938404febc604446..11c25e0845d6ccd327de6b508aceccbcf3d36f66 100644 (file)
 
 use std::collections::{HashMap, HashSet};
 use rand::seq::SliceRandom;
+use crate::algorithms::stable_matching::types::{Gender, Person, Preferences};
 
-/// Represents the gender of a person in the matching problem.
-/// 
-/// This enum is used to distinguish between the two sides of the bipartite matching.
-/// In the classical formulation, these are typically "men" and "women", but the
-/// algorithm applies to any two-sided matching scenario.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Gender {
-    Male,
-    Female,
-}
-
-/// A person participating in the stable matching problem.
-///
-/// Each person has a unique identifier and belongs to one of two groups
-/// distinguished by gender. The algorithm ensures each person from one
-/// group is matched with exactly one person from the other group.
-///
-/// ## Type Theory Note
-/// This represents an element in one of two disjoint sets that form
-/// the domain of our matching function.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Person {
-    pub id: u32,
-    pub gender: Gender,
-}
-
-/// Represents a person's ordered preference list over potential partners.
-///
-/// This structure encapsulates the total ordering required by the Gale-Shapley
-/// algorithm. Each person must have a complete, strict preference ordering
-/// over all potential partners.
-///
-/// ## Category Theory Note
-/// This represents a morphism in the category of preferences, where objects
-/// are people and morphisms represent preference relations.
-///
-/// ## Examples
-///
-/// ```
-/// # use algorithms::stable_matching::*;
-/// // Person 1 prefers partners in order: 3, 1, 2
-/// let prefs = Preferences::new(1, vec!)?;[1][2][3]
-/// 
-/// // Check if person 3 is preferred over person 2
-/// assert!(prefs.prefers(3, 2)?);
-/// # Ok::<(), &'static str>(())
-/// ```
-#[derive(Debug, Clone)]
-pub struct Preferences {
-    /// The ordered list of preferred partner IDs (most preferred first)    
-    pub ordered_ids: Vec<u32>,
-    /// The ID of the person who holds these preferences    
-    pub person_id: u32,
-}
-
-impl Preferences {
-
-    /// Creates a new preference list with validation.
-    ///
-    /// # Arguments
-    /// * `person_id` - The ID of the person who holds these preferences
-    /// * `ordered_ids` - List of preferred partner IDs in order of preference
-    ///
-    /// # Returns
-    /// * `Ok(Preferences)` - Valid preference list
-    /// * `Err(&'static str)` - Error message if validation fails
-    ///
-    /// # Errors
-    /// * Returns error if the preference list is empty
-    /// * Returns error if there are duplicate preferences
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// # use algorithms::stable_matching::*;    
-    /// # use crate::Preferences;
-    /// // Valid preferences
-    /// let prefs = Preferences::new(1, vec!)?;[2][3][4]
-    /// 
-    /// // Invalid - empty list
-    /// assert!(Preferences::new(1, vec![]).is_err());
-    /// 
-    /// // Invalid - duplicates
-    /// assert!(Preferences::new(1, vec!).is_err());[3][2]
-    /// # Ok::<(), &'static str>(())
-    /// ```    
-    pub fn new(person_id: u32, ordered_ids: Vec<u32>) -> Result<Self, &'static str> {
-        if ordered_ids.is_empty() {
-            return Err("Preference list cannot be empty");
-        }
-        
-        let mut unique_ids = ordered_ids.clone();
-        unique_ids.sort();
-        unique_ids.dedup();
-        if unique_ids.len() != ordered_ids.len() {
-            return Err("No duplicate preferences allowed");
-        }
-        
-        Ok(Preferences {
-            person_id,
-            ordered_ids,
-        })
-    }
-
-    /// Determines if person `a_id` is preferred over person `b_id`.
-    ///
-    /// This implements the strict preference relation required for stable matching.
-    /// Returns `true` if `a_id` appears earlier in the preference list than `b_id`.
-    ///
-    /// # Arguments
-    /// * `a_id` - ID of the first person to compare
-    /// * `b_id` - ID of the second person to compare
-    ///
-    /// # Returns
-    /// * `Ok(true)` - If `a_id` is preferred over `b_id`
-    /// * `Ok(false)` - If `b_id` is preferred over `a_id`
-    /// * `Err(&'static str)` - If either person is not in the preference list
-    ///
-    /// # Mathematical Note
-    /// This implements the relation `a_id ≻ b_id` in preference theory notation.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// # use algorithms::stable_matching::*;    
-    /// # use crate::Preferences;
-    /// let prefs = Preferences::new(1, vec!)?;[2][3][5]
-    /// 
-    /// assert!(prefs.prefers(3, 5)?);  // 3 is preferred over 5
-    /// assert!(!prefs.prefers(5, 3)?); // 5 is not preferred over 3
-    /// # Ok::<(), &'static str>(())
-    /// ```    
-    pub fn prefers(&self, a_id: u32, b_id: u32) -> Result<bool, &'static str> {
-        let pos_a = self.ordered_ids.iter().position(|&id| id == a_id)
-            .ok_or("Person A not found in preference list")?;
-        let pos_b = self.ordered_ids.iter().position(|&id| id == b_id)
-            .ok_or("Person B not found in preference list")?;
-        
-        Ok(pos_a < pos_b)
-    }
-
-    /// Returns the most preferred person's ID.
-    ///
-    /// # Returns
-    /// The ID of the person at the top of this preference list.
-    ///
-    /// # Panics
-    /// Panics if the preference list is empty (which should be impossible
-    /// if constructed through `new()`).
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// # use algorithms::stable_matching::*;    
-    /// # use crate::Preferences;
-    /// use algorithms::stable_matching::*;    
-    /// let prefs = Preferences::new(1, vec!)?;[7][3][5]
-    /// assert_eq!(prefs.most_preferred(), 7);
-    /// # Ok::<(), &'static str>(())
-    /// ```    
-    pub fn most_preferred(&self) -> u32 {
-        self.ordered_ids[0]
-    }
-}
 
 /// The main structure representing a stable matching problem instance.
 ///
index 37f5b56150ffb5e5da348b37885b35cd170595b0..98d710eba8bf8d8e2ca84daf4f15ac9ab479fe59 100644 (file)
@@ -4,8 +4,11 @@
 
 // Declare submodules if you split them
 pub mod gale_shapley;
+pub mod types;
+pub mod bipartite;
 //pub mod types;
 
 // Re-export the public API from submodules
 pub use gale_shapley::*;
-//pub use types::*;
+pub use bipartite::*;
+pub use types::*;
diff --git a/src/algorithms/stable_matching/types.rs b/src/algorithms/stable_matching/types.rs
new file mode 100644 (file)
index 0000000..0480f8e
--- /dev/null
@@ -0,0 +1,165 @@
+
+/// Represents the gender of a person in the matching problem.
+/// 
+/// This enum is used to distinguish between the two sides of the bipartite matching.
+/// In the classical formulation, these are typically "men" and "women", but the
+/// algorithm applies to any two-sided matching scenario.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Gender {
+    Male,
+    Female,
+}
+
+/// A person participating in the stable matching problem.
+///
+/// Each person has a unique identifier and belongs to one of two groups
+/// distinguished by gender. The algorithm ensures each person from one
+/// group is matched with exactly one person from the other group.
+///
+/// ## Type Theory Note
+/// This represents an element in one of two disjoint sets that form
+/// the domain of our matching function.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Person {
+    pub id: u32,
+    pub gender: Gender,
+}
+
+/// Represents a person's ordered preference list over potential partners.
+///
+/// This structure encapsulates the total ordering required by the Gale-Shapley
+/// algorithm. Each person must have a complete, strict preference ordering
+/// over all potential partners.
+///
+/// ## Category Theory Note
+/// This represents a morphism in the category of preferences, where objects
+/// are people and morphisms represent preference relations.
+///
+/// ## Examples
+///
+/// ```
+/// # use algorithms::stable_matching::*;
+/// // Person 1 prefers partners in order: 3, 1, 2
+/// let prefs = Preferences::new(1, vec!)?;[1][2][3]
+///
+/// // Check if person 3 is preferred over person 2
+/// assert!(prefs.prefers(3, 2)?);
+/// # Ok::<(), &'static str>(())
+/// ```
+#[derive(Debug, Clone)]
+pub struct Preferences {
+    /// The ordered list of preferred partner IDs (most preferred first)
+    pub ordered_ids: Vec<u32>,
+    /// The ID of the person who holds these preferences
+    pub person_id: u32,
+}
+
+impl Preferences {
+
+    /// Creates a new preference list with validation.
+    ///
+    /// # Arguments
+    /// * `person_id` - The ID of the person who holds these preferences
+    /// * `ordered_ids` - List of preferred partner IDs in order of preference
+    ///
+    /// # Returns
+    /// * `Ok(Preferences)` - Valid preference list
+    /// * `Err(&'static str)` - Error message if validation fails
+    ///
+    /// # Errors
+    /// * Returns error if the preference list is empty
+    /// * Returns error if there are duplicate preferences
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use algorithms::stable_matching::*;
+    /// # use crate::Preferences;
+    /// // Valid preferences
+    /// let prefs = Preferences::new(1, vec!)?;[2][3][4]
+    ///
+    /// // Invalid - empty list
+    /// assert!(Preferences::new(1, vec![]).is_err());
+    ///
+    /// // Invalid - duplicates
+    /// assert!(Preferences::new(1, vec!).is_err());[3][2]
+    /// # Ok::<(), &'static str>(())
+    /// ```
+    pub fn new(person_id: u32, ordered_ids: Vec<u32>) -> Result<Self, &'static str> {
+        if ordered_ids.is_empty() {
+            return Err("Preference list cannot be empty");
+        }
+
+        let mut unique_ids = ordered_ids.clone();
+        unique_ids.sort();
+        unique_ids.dedup();
+        if unique_ids.len() != ordered_ids.len() {
+            return Err("No duplicate preferences allowed");
+        }
+
+        Ok(Preferences {
+            person_id,
+            ordered_ids,
+        })
+    }
+
+    /// Determines if person `a_id` is preferred over person `b_id`.
+    ///
+    /// This implements the strict preference relation required for stable matching.
+    /// Returns `true` if `a_id` appears earlier in the preference list than `b_id`.
+    ///
+    /// # Arguments
+    /// * `a_id` - ID of the first person to compare
+    /// * `b_id` - ID of the second person to compare
+    ///
+    /// # Returns
+    /// * `Ok(true)` - If `a_id` is preferred over `b_id`
+    /// * `Ok(false)` - If `b_id` is preferred over `a_id`
+    /// * `Err(&'static str)` - If either person is not in the preference list
+    ///
+    /// # Mathematical Note
+    /// This implements the relation `a_id ≻ b_id` in preference theory notation.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use algorithms::stable_matching::*;
+    /// # use crate::Preferences;
+    /// let prefs = Preferences::new(1, vec!)?;[2][3][5]
+    ///
+    /// assert!(prefs.prefers(3, 5)?);  // 3 is preferred over 5
+    /// assert!(!prefs.prefers(5, 3)?); // 5 is not preferred over 3
+    /// # Ok::<(), &'static str>(())
+    /// ```
+    pub fn prefers(&self, a_id: u32, b_id: u32) -> Result<bool, &'static str> {
+        let pos_a = self.ordered_ids.iter().position(|&id| id == a_id)
+            .ok_or("Person A not found in preference list")?;
+        let pos_b = self.ordered_ids.iter().position(|&id| id == b_id)
+            .ok_or("Person B not found in preference list")?;
+
+        Ok(pos_a < pos_b)
+    }
+
+    /// Returns the most preferred person's ID.
+    ///
+    /// # Returns
+    /// The ID of the person at the top of this preference list.
+    ///
+    /// # Panics
+    /// Panics if the preference list is empty (which should be impossible
+    /// if constructed through `new()`).
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use algorithms::stable_matching::*;
+    /// # use crate::Preferences;
+    /// use algorithms::stable_matching::*;
+    /// let prefs = Preferences::new(1, vec!)?;[7][3][5]
+    /// assert_eq!(prefs.most_preferred(), 7);
+    /// # Ok::<(), &'static str>(())
+    /// ```
+    pub fn most_preferred(&self) -> u32 {
+        self.ordered_ids[0]
+    }
+}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644 (file)
index 1e9610a..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-//use algorithms::stable_matching::*;
-use algorithms::interview_scheduling::*;
-
-fn main() {
-    // Example usage of different algorithms
-    println!("Running algorithm examples...");
-
-    // Use stable matching
-    let config = InterviewGenConfig::default();
-    let interviews = generate_random_interviews(&config);
-    println!("Generated {} interviews", interviews.len());    
-}