234 lines
6.2 KiB
Go
234 lines
6.2 KiB
Go
![]() |
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)
|
||
|
}
|