From 9c62e053dc1f922877973e8f43489aaf76d50c45 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 16:45:08 +0100 Subject: [PATCH 1/2] Add solution to allocation laptop and sadness --- prep-exercises/.gitignore | 25 +++++++ prep-exercises/laptop_allocation.py | 104 ++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 prep-exercises/.gitignore create mode 100644 prep-exercises/laptop_allocation.py diff --git a/prep-exercises/.gitignore b/prep-exercises/.gitignore new file mode 100644 index 0000000..d300648 --- /dev/null +++ b/prep-exercises/.gitignore @@ -0,0 +1,25 @@ +# Virtual Environment +.venv/ +venv/ +.env/ +env/ +ENV/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# mypy cache +.mypy_cache/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store + diff --git a/prep-exercises/laptop_allocation.py b/prep-exercises/laptop_allocation.py new file mode 100644 index 0000000..24e1402 --- /dev/null +++ b/prep-exercises/laptop_allocation.py @@ -0,0 +1,104 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List, Dict + +class OperatingSystem(Enum): + MACOS = "macOS" + ARCH = "Arch Linux" + UBUNTU = "Ubuntu" + +@dataclass(frozen=True) +class Person: + name: str + age: int + # Sorted in order of preference, most preferred is first. + # Using tuple instead of List to make Person hashable (for dict keys) + preferred_operating_system: tuple[OperatingSystem, ...] + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + + +def calculate_sadness(person: Person, laptop: Laptop) -> int: + """Calculate sadness score for a person-laptop pairing.""" + try: + return person.preferred_operating_system.index(laptop.operating_system) + except ValueError: + return 100 + + +def allocate_laptops(people: List[Person], laptops: List[Laptop]) -> Dict[Person, Laptop]: + """ + Allocate laptops to people minimizing total sadness. + + Sadness is defined as: + - Index in preference list (0 for first choice, 1 for second, etc.) + - 100 if the OS is not in their preference list + """ + if len(people) != len(laptops): + raise ValueError("Number of people must equal number of laptops") + + # Greedy approach: Sort people by how limited their good options are + # Then allocate their best available choice + allocation: Dict[Person, Laptop] = {} + available_laptops = list(laptops) + + # Create a priority queue of (person, laptop, sadness) tuples + # Sort by sadness to allocate best matches first + preferences = [] + for person in people: + for laptop in available_laptops: + sadness = calculate_sadness(person, laptop) + preferences.append((sadness, person, laptop)) + + preferences.sort(key=lambda x: x[0]) + + # Greedy allocation: try to give everyone their best available choice + allocated_people = [] + allocated_laptops = [] + + for sadness, person, laptop in preferences: + if person not in allocated_people and laptop not in allocated_laptops: + allocation[person] = laptop + allocated_people.append(person) + allocated_laptops.append(laptop) + + if len(allocation) == len(people): + break + + return allocation + + +def calculate_total_sadness(allocation: Dict[Person, Laptop]) -> int: + """Calculate total sadness for an allocation.""" + return sum(calculate_sadness(person, laptop) for person, laptop in allocation.items()) + + +# Test the function +people = [ + Person("Alice", 25, (OperatingSystem.UBUNTU, OperatingSystem.ARCH, OperatingSystem.MACOS)), + Person("Bob", 30, (OperatingSystem.MACOS, OperatingSystem.UBUNTU)), + Person("Charlie", 28, (OperatingSystem.ARCH, OperatingSystem.UBUNTU)), +] + +laptops = [ + Laptop(1, "Dell", "XPS", 13, OperatingSystem.UBUNTU), + Laptop(2, "Apple", "MacBook", 13, OperatingSystem.MACOS), + Laptop(3, "Lenovo", "ThinkPad", 14, OperatingSystem.ARCH), +] + +allocation = allocate_laptops(people, laptops) + +print("Laptop Allocation:") +for person, laptop in allocation.items(): + sadness = calculate_sadness(person, laptop) + print(f"{person.name} -> {laptop.manufacturer} {laptop.model} ({laptop.operating_system.value}) - Sadness: {sadness}") + +print(f"\nTotal Sadness: {calculate_total_sadness(allocation)}") + From 7b7aafa696a28ecb38efb2ec6727085887ccf044 Mon Sep 17 00:00:00 2001 From: Alaa Date: Wed, 29 Oct 2025 07:59:42 +0000 Subject: [PATCH 2/2] Refactor: eliminate redundant loops & total sadness calculation into single loop --- prep-exercises/laptop_allocation.py | 50 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/prep-exercises/laptop_allocation.py b/prep-exercises/laptop_allocation.py index 24e1402..a23d1d0 100644 --- a/prep-exercises/laptop_allocation.py +++ b/prep-exercises/laptop_allocation.py @@ -44,33 +44,41 @@ def allocate_laptops(people: List[Person], laptops: List[Laptop]) -> Dict[Person if len(people) != len(laptops): raise ValueError("Number of people must equal number of laptops") - # Greedy approach: Sort people by how limited their good options are - # Then allocate their best available choice allocation: Dict[Person, Laptop] = {} - available_laptops = list(laptops) + available_laptops = set(laptops) # Use set for O(1) lookup and removal - # Create a priority queue of (person, laptop, sadness) tuples - # Sort by sadness to allocate best matches first - preferences = [] - for person in people: + # Helper to find best available laptop for a person + def find_best_available(person: Person) -> tuple[Laptop, int] | None: + """Returns (best_laptop, sadness) or None if no laptops available.""" + best_laptop = None + best_sadness = float('inf') + for laptop in available_laptops: sadness = calculate_sadness(person, laptop) - preferences.append((sadness, person, laptop)) + if sadness < best_sadness: + best_sadness = sadness + best_laptop = laptop + # Early exit if we found a perfect match (sadness 0) + if sadness == 0: + break + + return (best_laptop, best_sadness) if best_laptop else None - preferences.sort(key=lambda x: x[0]) + # Sort people by how limited their good options are + # (by counting how many laptops match their preferences - less matches = higher priority) + def count_good_matches(person: Person) -> int: + """Count how many laptops have sadness < 100 (i.e., OS is in their preferences).""" + return sum(1 for laptop in laptops if calculate_sadness(person, laptop) < 100) - # Greedy allocation: try to give everyone their best available choice - allocated_people = [] - allocated_laptops = [] + sorted_people = sorted(people, key=count_good_matches) - for sadness, person, laptop in preferences: - if person not in allocated_people and laptop not in allocated_laptops: + # Greedy allocation: for each person, find their best available laptop + for person in sorted_people: + result = find_best_available(person) + if result: + laptop, _ = result allocation[person] = laptop - allocated_people.append(person) - allocated_laptops.append(laptop) - - if len(allocation) == len(people): - break + available_laptops.remove(laptop) # O(1) removal return allocation @@ -96,9 +104,11 @@ def calculate_total_sadness(allocation: Dict[Person, Laptop]) -> int: allocation = allocate_laptops(people, laptops) print("Laptop Allocation:") +total_sadness = 0 for person, laptop in allocation.items(): sadness = calculate_sadness(person, laptop) + total_sadness += sadness print(f"{person.name} -> {laptop.manufacturer} {laptop.model} ({laptop.operating_system.value}) - Sadness: {sadness}") -print(f"\nTotal Sadness: {calculate_total_sadness(allocation)}") +print(f"\nTotal Sadness: {total_sadness}")