From e92edcb8a9f55ee29c5f183ffc78e37b7c0fa937 Mon Sep 17 00:00:00 2001 From: Vinzenz Stadtmueller Date: Wed, 12 Feb 2025 19:56:11 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + docs/index.md | 17 ++++ go.mod | 3 + go.sum | 0 library.go | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 47 ++++++++++ mkdocs.yml | 1 + 7 files changed, 303 insertions(+) create mode 100644 .gitignore create mode 100644 docs/index.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 library.go create mode 100644 main.go create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..314f02b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.txt \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..000ea34 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Welcome to MkDocs + +For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs -h` - Print help message and exit. + +## Project layout + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cb343b4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module workshop + +go 1.23.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/library.go b/library.go new file mode 100644 index 0000000..2996a5b --- /dev/null +++ b/library.go @@ -0,0 +1,234 @@ +package main + +import ( + "fmt" + "sync" + "time" + "log/slog" +) + +// Participant struct +type Participant struct { + ID int + Preferences []int + AssignedTo int + RejectedFrom map[int]bool + mu sync.Mutex +} + +// Workshop struct +type Workshop struct { + ID int + Capacity int + Participants []int + mu sync.Mutex +} + +// Concurrent Stable Matching Algorithm +func StableMatch(participants []Participant, workshops []Workshop) { + var wg sync.WaitGroup + numWorkers := 10 + tasks := make(chan *Participant, len(participants)) + + worker := func() { + defer wg.Done() + for p := range tasks { + assignParticipant(p, &workshops, participants) + } + } + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go worker() + } + + for i := range participants { + tasks <- &participants[i] + } + close(tasks) + + wg.Wait() +} + +// Assign a participant to a workshop +func assignParticipant(p *Participant, workshops *[]Workshop, participants []Participant) { + p.mu.Lock() + defer p.mu.Unlock() + + assigned := false + + for _, workshopID := range p.Preferences { + if p.RejectedFrom[workshopID] { + continue + } + + if workshopID-1 < 0 || workshopID-1 >= len(*workshops) { + continue // Prevent index out of range + } + + w := &(*workshops)[workshopID-1] + w.mu.Lock() + + if len(w.Participants) < w.Capacity { + p.AssignedTo = workshopID + w.Participants = append(w.Participants, p.ID) + w.mu.Unlock() + assigned = true + break + } else { + weakestID, weakestIndex := findWeakestParticipant(w.Participants, participants, workshopID) + if weakestIndex != -1 && isPreferred(p.ID, weakestID, workshopID, participants) { + replaceParticipant(weakestID, p, w, weakestIndex, participants) + w.mu.Unlock() + assigned = true + break + } else { + p.RejectedFrom[workshopID] = true + } + } + w.mu.Unlock() + } + + if !assigned { + for i := range *workshops { + w := &(*workshops)[i] + w.mu.Lock() + if len(w.Participants) < w.Capacity { + p.AssignedTo = w.ID + w.Participants = append(w.Participants, p.ID) + w.mu.Unlock() + slog.Info("Participant is assigned to Workshop as fallback\n", slog.Int("ParticipantId", p.ID), slog.Int("WorkshopId", w.ID)) + assigned = true + break + } + w.mu.Unlock() + } + } + + if !assigned { + slog.Info("āŒ Participant could not be assigned\n", slog.Int("ParticipantId", p.ID)) + p.AssignedTo = -1 + } +} + +// Replace weakest participant in a workshop +func replaceParticipant(weakestID int, newP *Participant, w *Workshop, weakestIndex int, participants []Participant) { + for i, id := range w.Participants { + if id == weakestID { + w.Participants = append(w.Participants[:i], w.Participants[i+1:]...) + break + } + } + w.Participants[weakestIndex] = newP.ID + newP.AssignedTo = w.ID + participants[weakestID-1].AssignedTo = -1 + participants[weakestID-1].RejectedFrom[w.ID] = true +} + +// Find the weakest participant in a workshop +func findWeakestParticipant(participantIDs []int, participants []Participant, workshopID int) (int, int) { + weakestID := -1 + weakestIndex := -1 + lowestRank := 4 + + for i, id := range participantIDs { + if id-1 < 0 || id-1 >= len(participants) { + continue + } + for rank, pref := range participants[id-1].Preferences { + if pref == workshopID && rank < lowestRank { + lowestRank = rank + weakestID = id + weakestIndex = i + } + } + } + + return weakestID, weakestIndex +} + +// Check if participant A is preferred over B for a workshop +func isPreferred(aID, bID, workshopID int, participants []Participant) bool { + if aID-1 < 0 || aID-1 >= len(participants) || bID-1 < 0 || bID-1 >= len(participants) { + return false + } + + aRank, bRank := 4, 4 + + for rank, pref := range participants[aID-1].Preferences { + if pref == workshopID { + aRank = rank + } + } + for rank, pref := range participants[bID-1].Preferences { + if pref == workshopID { + bRank = rank + } + } + + return aRank < bRank +} + +// Print final assignments +func PrintAssignments(participants []Participant, workshops []Workshop) { + fmt.Println("\nFinal Assignments:") + for _, p := range participants { + fmt.Printf("Participant %d → Workshop %d\n", p.ID, p.AssignedTo) + } + + fmt.Println("\nWorkshops and their Participants:") + for _, w := range workshops { + fmt.Printf("Workshop %d: %v\n", w.ID, w.Participants) + } +} + +// Print statistics +func PrintStatistics(participants []Participant, workshops []Workshop, duration time.Duration) { + totalParticipants := len(participants) + totalWorkshops := len(workshops) + assignedCount := 0 + unassignedCount := 0 + firstChoiceCount := 0 + secondChoiceCount := 0 + thirdChoiceCount := 0 + fallbackCount := 0 + + // Count participant assignment stats + for _, p := range participants { + if p.AssignedTo != -1 { + assignedCount++ + + // Check which preference was assigned + if p.AssignedTo == p.Preferences[0] { + firstChoiceCount++ + } else if p.AssignedTo == p.Preferences[1] { + secondChoiceCount++ + } else if p.AssignedTo == p.Preferences[2] { + thirdChoiceCount++ + } else { + fallbackCount++ + } + } else { + unassignedCount++ + } + } + + // Calculate workshop utilization + fmt.Println("\nWorkshop Utilization:") + for _, w := range workshops { + usage := float64(len(w.Participants)) / float64(w.Capacity) * 100 + fmt.Printf("Workshop %d: %d/%d participants (%.2f%% full)\n", w.ID, len(w.Participants), w.Capacity, usage) + } + + // Print summary statistics + fmt.Println("\nšŸ”¹ Matching Statistics:") + fmt.Printf("ā³ Runtime: %d ms\n", duration.Milliseconds()) + fmt.Printf("šŸ“Œ Total Participants: %d\n", totalParticipants) + fmt.Printf("šŸ« Total Workshops: %d\n", totalWorkshops) + fmt.Printf("āœ… Assigned Participants: %d (%.2f%%)\n", assignedCount, float64(assignedCount)/float64(totalParticipants)*100) + fmt.Printf("āŒ Unassigned Participants: %d (%.2f%%)\n", unassignedCount, float64(unassignedCount)/float64(totalParticipants)*100) + fmt.Printf("šŸ„‡ First Choice Assigned: %d (%.2f%%)\n", firstChoiceCount, float64(firstChoiceCount)/float64(totalParticipants)*100) + fmt.Printf("🄈 Second Choice Assigned: %d (%.2f%%)\n", secondChoiceCount, float64(secondChoiceCount)/float64(totalParticipants)*100) + fmt.Printf("šŸ„‰ Third Choice Assigned: %d (%.2f%%)\n", thirdChoiceCount, float64(thirdChoiceCount)/float64(totalParticipants)*100) + fmt.Printf("āš ļø Assigned via Fallback: %d (%.2f%%)\n", fallbackCount, float64(fallbackCount)/float64(totalParticipants)*100) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..cb11833 --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "time" + + "math/rand" +) + +// Initialize participants and workshops +func initializeParticipantsAndWorkshops(numParticipants, numWorkshops int) ([]Participant, []Workshop) { + rand.Seed(time.Now().UnixNano()) + + participants := make([]Participant, numParticipants) + for i := range participants { + prefs := rand.Perm(numWorkshops)[:3] // Pick 3 unique workshops + participants[i] = Participant{ + ID: i + 1, + Preferences: []int{prefs[0] + 1, prefs[1] + 1, prefs[2] + 1}, // Convert to 1-based index + AssignedTo: -1, + RejectedFrom: make(map[int]bool), + } + } + + workshops := make([]Workshop, numWorkshops) + for i := range workshops { + workshops[i] = Workshop{ + ID: i + 1, + Capacity: rand.Intn(10) + 15, // Capacity between 15 and 25 + Participants: []int{}, + } + } + + return participants, workshops +} + +// Main function +func main() { + numParticipants := 600 + numWorkshops := 30 + + participants, workshops := initializeParticipantsAndWorkshops(numParticipants, numWorkshops) + startTime := time.Now() + StableMatch(participants, workshops) + duration := time.Since(startTime) + PrintAssignments(participants, workshops) + PrintStatistics(participants, workshops, duration) +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c97182f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1 @@ +site_name: My Docs