Gale_shapely / gale_shapley.py
daemon03's picture
Initial commit: Streamlit UI for Gale-Shapley Algorithm
f804bd5
"""
Gale-Shapley Algorithm Implementation
Python port of the C implementation from the original project.
Implements the Stable Marriage Problem for roommate matching and room allocation.
Original C version: https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git
"""
from typing import Dict, List, Tuple, Optional
def gale_shapley(pref_matrix: List[List[int]], n: int) -> Dict[int, int]:
"""
Run the Gale-Shapley algorithm for stable matching.
This is a direct Python translation of the C galeShapley() function.
Students 0..n-1 are "proposers" and students n..2n-1 are "acceptors".
Args:
pref_matrix: 2D preference list. pref_matrix[i] is the ordered preference
list for person i. For proposers (0..n-1), preferences are
indices in the acceptor range (n..2n-1). For acceptors,
preferences are proposer indices (0..n-1).
n: Number of pairs (half the total participants).
Returns:
Dictionary mapping each person to their matched partner.
"""
# match[i] = partner of person i, -1 means unmatched
match = [-1] * (2 * n)
free_count = n
# Track which preference index each proposer is up to
next_proposal = [0] * n
while free_count > 0:
# Find the first free proposer
m = -1
for i in range(n):
if match[i] == -1:
m = i
break
if m == -1:
break
# Proposer m proposes to their next preferred acceptor
while next_proposal[m] < n:
w = pref_matrix[m][next_proposal[m]]
next_proposal[m] += 1
if match[w] == -1:
# w is free, accept the proposal
match[w] = m
match[m] = w
free_count -= 1
break
else:
# w is already matched, check if w prefers m over current partner
m1 = match[w]
if _prefers_new_over_current(pref_matrix, w, m, m1):
# w prefers m over m1 — switch
match[w] = m
match[m] = w
match[m1] = -1 # m1 becomes free
break
# else w rejects m, m tries next preference
return match
def _prefers_new_over_current(
pref_matrix: List[List[int]], w: int, m_new: int, m_current: int
) -> bool:
"""
Check if acceptor w prefers m_new over m_current.
Mirrors the C function wPrefersm1Overm() but with clearer naming.
Returns True if w prefers m_new over m_current.
"""
for pref in pref_matrix[w]:
if pref == m_new:
return True
if pref == m_current:
return False
return False
def run_roommate_matching(students: List[dict]) -> List[Tuple[int, int]]:
"""
Stage 1: Match students into roommate pairs using Gale-Shapley.
Args:
students: List of student dicts with 'id', 'name', 'cgpa', 'pref_roommate'.
Returns:
List of (student_id_1, student_id_2) roommate pairs.
"""
n_students = len(students)
if n_students < 2 or n_students % 2 != 0:
raise ValueError(
f"Need an even number of students (≥ 2). Got {n_students}."
)
n = n_students // 2 # Number of pairs
# Build preference matrix: proposers are 0..n-1, acceptors are n..2n-1
# Each student's pref_roommate contains IDs of other students they prefer
pref_matrix = []
for student in students:
prefs = student["pref_roommate"]
# Filter out only valid preferences (should be IDs of other students)
# For proposers (0..n-1): their prefs should reference acceptors (n..2n-1)
# For acceptors (n..2n-1): their prefs should reference proposers (0..n-1)
sid = student["id"]
if sid < n:
# Proposer: filter prefs to only include acceptor IDs (n..2n-1)
filtered = [p for p in prefs if n <= p < 2 * n]
else:
# Acceptor: filter prefs to only include proposer IDs (0..n-1)
filtered = [p for p in prefs if 0 <= p < n]
pref_matrix.append(filtered)
# Run Gale-Shapley
match = gale_shapley(pref_matrix, n)
# Extract unique pairs (only from proposer side to avoid duplicates)
pairs = []
seen = set()
for i in range(n):
partner = match[i]
if partner != -1 and i not in seen and partner not in seen:
pairs.append((i, partner))
seen.add(i)
seen.add(partner)
return pairs
def run_room_allocation(
students: List[dict],
roommate_pairs: List[Tuple[int, int]],
rooms: List[dict],
) -> List[dict]:
"""
Stage 2: Allocate rooms to roommate pairs based on CGPA ranking.
Higher CGPA pairs get priority in room selection (Gale-Shapley on rooms).
Args:
students: List of student dicts with 'id', 'name', 'cgpa', 'pref_room'.
roommate_pairs: List of (id1, id2) roommate pairs from Stage 1.
rooms: List of room dicts with 'room_id' and 'room_number'.
Returns:
List of allocation dicts: {roommate1, roommate2, room_number, room_id, pair_cgpa}
"""
n = len(roommate_pairs)
n_rooms = len(rooms)
if n_rooms < n:
raise ValueError(
f"Not enough rooms ({n_rooms}) for {n} pairs."
)
# Build student lookup
student_map = {s["id"]: s for s in students}
# Rank pairs by the higher CGPA in each pair (descending)
pair_cgpas = []
for id1, id2 in roommate_pairs:
cgpa1 = student_map[id1]["cgpa"]
cgpa2 = student_map[id2]["cgpa"]
max_cgpa = max(cgpa1, cgpa2)
pair_cgpas.append((max_cgpa, id1, id2))
# Sort descending by max CGPA
pair_cgpas.sort(key=lambda x: x[0], reverse=True)
# Build room preference matrix for Gale-Shapley
# Proposers: ranked pairs (0..n-1) → these map to pair_cgpas indices
# Acceptors: rooms (n..2n-1) → these map to rooms indices (offset by n)
room_id_to_index = {rooms[j]["room_id"]: j + n for j in range(n)}
pref_matrix = [[] for _ in range(2 * n)]
# For each ranked pair, get the higher-CGPA student's room preferences
for rank_idx, (max_cgpa, id1, id2) in enumerate(pair_cgpas):
# Use the higher CGPA student's room preferences
if student_map[id1]["cgpa"] >= student_map[id2]["cgpa"]:
prefs = student_map[id1].get("pref_room", [])
else:
prefs = student_map[id2].get("pref_room", [])
# Map room IDs to room indices (offset by n for acceptor range)
mapped_prefs = []
for room_id in prefs:
if room_id in room_id_to_index:
mapped_prefs.append(room_id_to_index[room_id])
pref_matrix[rank_idx] = mapped_prefs
# Rooms accept any student in order (no real preference)
for j in range(n):
pref_matrix[n + j] = list(range(n))
# Run Gale-Shapley for room allocation
match = gale_shapley(pref_matrix, n)
# Build index-to-room mapping
index_to_room = {j + n: rooms[j] for j in range(n)}
# Build final allocation
allocations = []
for rank_idx, (max_cgpa, id1, id2) in enumerate(pair_cgpas):
room_index = match[rank_idx]
room = index_to_room.get(room_index, {"room_number": "N/A", "room_id": -1})
allocations.append({
"roommate1_id": id1,
"roommate1_name": student_map[id1]["name"],
"roommate1_cgpa": student_map[id1]["cgpa"],
"roommate2_id": id2,
"roommate2_name": student_map[id2]["name"],
"roommate2_cgpa": student_map[id2]["cgpa"],
"room_number": room["room_number"],
"room_id": room["room_id"],
"pair_max_cgpa": max_cgpa,
})
return allocations
def run_full_allocation(students: List[dict], rooms: List[dict]) -> List[dict]:
"""
Run the complete two-stage allocation pipeline.
Stage 1: Gale-Shapley for roommate matching.
Stage 2: CGPA-ranked Gale-Shapley for room allocation.
Args:
students: List of student dicts.
rooms: List of room dicts.
Returns:
List of allocation result dicts.
"""
# Stage 1: Roommate matching
roommate_pairs = run_roommate_matching(students)
# Stage 2: Room allocation
allocations = run_room_allocation(students, roommate_pairs, rooms)
return allocations