mirror of
https://github.com/sabazadam/Se302.git
synced 2025-12-31 20:31:22 +00:00
1280 lines
40 KiB
Markdown
1280 lines
40 KiB
Markdown
# ALGORITHM DESIGN - EXAM SCHEDULING SYSTEM
|
||
|
||
**Project:** Exam Planner Desktop Application
|
||
**Algorithm:** Backtracking CSP Solver with Constraint Propagation
|
||
**Version:** 1.0
|
||
**Last Updated:** November 26, 2025
|
||
|
||
---
|
||
|
||
## TABLE OF CONTENTS
|
||
|
||
1. [Overview](#overview)
|
||
2. [Problem Formulation](#problem-formulation)
|
||
3. [Algorithm Architecture](#algorithm-architecture)
|
||
4. [Backtracking Algorithm](#backtracking-algorithm)
|
||
5. [Constraint Validation](#constraint-validation)
|
||
6. [Heuristics](#heuristics)
|
||
7. [Optimization Strategies](#optimization-strategies)
|
||
8. [Conflict Detection](#conflict-detection)
|
||
9. [Performance Optimizations](#performance-optimizations)
|
||
10. [Implementation Guide](#implementation-guide)
|
||
11. [Testing Strategy](#testing-strategy)
|
||
12. [Complexity Analysis](#complexity-analysis)
|
||
|
||
---
|
||
|
||
## OVERVIEW
|
||
|
||
### Problem Statement
|
||
|
||
Given:
|
||
- **N courses** that need exam scheduling
|
||
- **M classrooms** with specific capacities
|
||
- **S students** enrolled in various courses
|
||
- **Exam period configuration** (D days, T time slots per day)
|
||
|
||
Find:
|
||
- An assignment of each course to exactly one {Classroom, Day, TimeSlot} triple
|
||
|
||
Such that:
|
||
- **Hard Constraint 1:** No student has consecutive exams
|
||
- **Hard Constraint 2:** No student has more than 2 exams per day
|
||
- **Hard Constraint 3:** Classroom capacity is not exceeded
|
||
- **Implicit Constraint 4:** No classroom double-booking
|
||
|
||
### Algorithm Choice: Backtracking CSP
|
||
|
||
**Why Backtracking?**
|
||
- ✅ **Completeness:** Guarantees finding solution if one exists
|
||
- ✅ **Soundness:** Only returns valid solutions
|
||
- ✅ **Systematic:** Explores search space methodically
|
||
- ✅ **Constraint-friendly:** Natural fit for hard constraints
|
||
|
||
**Why Not Greedy?**
|
||
- ❌ May fail even when solution exists
|
||
- ❌ No guarantee of correctness
|
||
- ✅ Faster but less reliable
|
||
|
||
**Why Not Genetic Algorithm?**
|
||
- ❌ Probabilistic (no guarantee)
|
||
- ❌ Complex to ensure 100% constraint satisfaction
|
||
- ✅ Good for optimization but not for hard constraints
|
||
|
||
**Decision:** Backtracking with heuristics provides best balance of correctness and performance.
|
||
|
||
---
|
||
|
||
## PROBLEM FORMULATION
|
||
|
||
### CSP Components
|
||
|
||
#### 1. Variables
|
||
|
||
**Definition:** Each course is a variable
|
||
|
||
```
|
||
Variables = {Course_1, Course_2, ..., Course_N}
|
||
```
|
||
|
||
**Example:**
|
||
```
|
||
Variables = {CourseCode_01, CourseCode_02, ..., CourseCode_20}
|
||
N = 20
|
||
```
|
||
|
||
---
|
||
|
||
#### 2. Domain
|
||
|
||
**Definition:** For each course, the domain is the set of all possible {Classroom, Day, TimeSlot} assignments
|
||
|
||
```
|
||
Domain(Course_i) = {(c, d, t) | c ∈ Classrooms,
|
||
d ∈ {1, 2, ..., D},
|
||
t ∈ {1, 2, ..., T}}
|
||
```
|
||
|
||
**Example:**
|
||
```
|
||
Classrooms = {Classroom_01, ..., Classroom_10} # 10 classrooms
|
||
Days = {1, 2, 3, 4, 5} # 5 days
|
||
TimeSlots = {1, 2, 3, 4} # 4 slots per day
|
||
|
||
Domain(CourseCode_01) = {
|
||
(Classroom_01, 1, 1), # Classroom 1, Day 1, Slot 1
|
||
(Classroom_01, 1, 2), # Classroom 1, Day 1, Slot 2
|
||
...,
|
||
(Classroom_10, 5, 4) # Classroom 10, Day 5, Slot 4
|
||
}
|
||
|
||
|Domain| = 10 × 5 × 4 = 200 possible assignments per course
|
||
```
|
||
|
||
**Domain Filtering (Pre-processing):**
|
||
|
||
Before backtracking, filter out invalid assignments:
|
||
|
||
```java
|
||
Domain(Course_i) = {(c, d, t) | enrolledCount(Course_i) ≤ capacity(c)}
|
||
```
|
||
|
||
**Example:**
|
||
```
|
||
CourseCode_01 has 40 students enrolled
|
||
Classroom_01 has capacity 40 → Valid ✓
|
||
Classroom_02 has capacity 35 → Invalid ✗ (removed from domain)
|
||
```
|
||
|
||
After filtering, domain size reduced significantly (only valid classrooms remain).
|
||
|
||
---
|
||
|
||
#### 3. Constraints
|
||
|
||
**Unary Constraints (Pre-processing):**
|
||
```
|
||
enrolledCount(Course_i) ≤ capacity(Classroom)
|
||
```
|
||
|
||
**Binary Constraints (Between variables):**
|
||
|
||
**Constraint 1: No Consecutive Exams**
|
||
```
|
||
For any two courses c1, c2 with shared students:
|
||
If c1 assigned to (classroom1, day1, slot1)
|
||
And c2 assigned to (classroom2, day2, slot2)
|
||
Then: NOT consecutive(day1, slot1, day2, slot2)
|
||
```
|
||
|
||
**Constraint 2: Max 2 Exams Per Day**
|
||
```
|
||
For any student s:
|
||
Count of exams on any day d ≤ 2
|
||
```
|
||
|
||
**Constraint 3: No Classroom Double-Booking (Implicit)**
|
||
```
|
||
For any two courses c1 ≠ c2:
|
||
If c1 assigned to (classroom, day, slot)
|
||
Then c2 cannot be assigned to (classroom, day, slot)
|
||
```
|
||
|
||
---
|
||
|
||
### Search Space Size
|
||
|
||
**Theoretical maximum:**
|
||
```
|
||
Search Space = |Domain|^N
|
||
= 200^20
|
||
= 1.024 × 10^46 possible assignments
|
||
```
|
||
|
||
**With constraint propagation and heuristics:**
|
||
```
|
||
Effective search space << 10^46
|
||
```
|
||
|
||
Heuristics reduce this dramatically (typically explore < 1000 nodes for sample data).
|
||
|
||
---
|
||
|
||
## ALGORITHM ARCHITECTURE
|
||
|
||
### High-Level Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ CSPSolver.solve() │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 1. Pre-processing │
|
||
│ • Load data (courses, classrooms, enrollments) │
|
||
│ • Build enrollment maps (student→courses, course→students) │
|
||
│ • Filter domains (remove invalid classrooms) │
|
||
│ • Check basic feasibility (enough slots?) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 2. Backtracking Search │
|
||
│ WHILE unassigned courses exist: │
|
||
│ • Select next course (MRV heuristic) │
|
||
│ • Order domain values (Least Constraining Value) │
|
||
│ • Try each value: │
|
||
│ - Check constraints │
|
||
│ - If valid: assign and recurse │
|
||
│ - If fails: backtrack │
|
||
│ • If no value works: return FAILURE │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────┴───────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────┐ ┌──────────────┐
|
||
│ SUCCESS │ │ FAILURE │
|
||
│ Return │ │ Analyze │
|
||
│ Schedule │ │ Conflicts │
|
||
└─────────────┘ └──────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### Component Diagram
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ CSPSolver │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ + solve(courses, classrooms, enrollments, config, strategy) │
|
||
│ - backtrack(assignment, domains) │
|
||
│ - selectUnassignedVariable() │
|
||
│ - orderDomainValues(course, strategy) │
|
||
│ - forwardCheck(course, assignment) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│
|
||
│ uses
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ ConstraintValidator │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ + isValid(course, classroom, day, slot, assignment) │
|
||
│ + checkNoConsecutiveExams(...) │
|
||
│ + checkMaxTwoPerDay(...) │
|
||
│ + checkClassroomCapacity(...) │
|
||
│ + getViolations(...) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│
|
||
│ uses
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ OptimizationStrategy │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ + compareAssignments(a1, a2) // for value ordering │
|
||
│ + evaluateAssignment(assignment) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│
|
||
│ implementations
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ MinimizeDaysStrategy | BalanceDistributionStrategy │
|
||
│ MinimizeClassroomsStrategy | BalanceClassroomsStrategy │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## BACKTRACKING ALGORITHM
|
||
|
||
### Pseudocode (High-Level)
|
||
|
||
```
|
||
FUNCTION CSPSolver.solve(courses, classrooms, enrollments, config, strategy):
|
||
// Pre-processing
|
||
enrollmentMap ← buildEnrollmentMap(enrollments)
|
||
domains ← initializeDomains(courses, classrooms, enrollmentMap)
|
||
|
||
// Check basic feasibility
|
||
IF count(courses) > config.totalSlots THEN
|
||
THROW NotEnoughSlotsException
|
||
END IF
|
||
|
||
FOR EACH course IN courses DO
|
||
IF domains[course].isEmpty() THEN
|
||
THROW NoValidClassroomException(course)
|
||
END IF
|
||
END FOR
|
||
|
||
// Start backtracking search
|
||
assignment ← {} // Empty assignment
|
||
result ← BACKTRACK(assignment, domains, enrollmentMap, strategy)
|
||
|
||
IF result = FAILURE THEN
|
||
conflicts ← ConflictDetector.analyze(courses, classrooms, enrollments, config)
|
||
THROW NoSolutionException(conflicts)
|
||
ELSE
|
||
RETURN buildSchedule(result)
|
||
END IF
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
### Pseudocode (Detailed Backtracking)
|
||
|
||
```
|
||
FUNCTION BACKTRACK(assignment, domains, enrollmentMap, strategy):
|
||
// Base case: all courses assigned
|
||
IF assignment.isComplete() THEN
|
||
RETURN assignment // SUCCESS
|
||
END IF
|
||
|
||
// Select next unassigned course (MRV heuristic)
|
||
course ← SELECT_UNASSIGNED_VARIABLE(assignment, domains)
|
||
|
||
// Order domain values (Least Constraining Value + Strategy)
|
||
orderedValues ← ORDER_DOMAIN_VALUES(course, domains[course], assignment, strategy)
|
||
|
||
FOR EACH (classroom, day, slot) IN orderedValues DO
|
||
// Check if this assignment violates any constraints
|
||
IF CONSTRAINTS_SATISFIED(course, classroom, day, slot, assignment, enrollmentMap) THEN
|
||
// Make assignment
|
||
assignment[course] ← (classroom, day, slot)
|
||
|
||
// Forward checking: update domains of unassigned courses
|
||
removedValues ← FORWARD_CHECK(course, classroom, day, slot, assignment, domains, enrollmentMap)
|
||
|
||
IF NOT removedValues.causedEmptyDomain() THEN
|
||
// Recurse
|
||
result ← BACKTRACK(assignment, domains, enrollmentMap, strategy)
|
||
|
||
IF result ≠ FAILURE THEN
|
||
RETURN result // Found solution!
|
||
END IF
|
||
END IF
|
||
|
||
// Backtrack: undo assignment and restore domains
|
||
DELETE assignment[course]
|
||
RESTORE_DOMAINS(domains, removedValues)
|
||
END IF
|
||
END FOR
|
||
|
||
RETURN FAILURE // No valid assignment for this course
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
### Key Functions Explained
|
||
|
||
#### SELECT_UNASSIGNED_VARIABLE (MRV Heuristic)
|
||
|
||
**Minimum Remaining Values:** Choose the course with fewest valid domain values remaining.
|
||
|
||
```
|
||
FUNCTION SELECT_UNASSIGNED_VARIABLE(assignment, domains):
|
||
minCourse ← NULL
|
||
minDomainSize ← INFINITY
|
||
|
||
FOR EACH course IN courses DO
|
||
IF course NOT IN assignment THEN // Unassigned
|
||
domainSize ← count(domains[course])
|
||
|
||
IF domainSize < minDomainSize THEN
|
||
minDomainSize ← domainSize
|
||
minCourse ← course
|
||
ELSE IF domainSize = minDomainSize THEN
|
||
// Tie-breaking: choose course with most students (harder to place)
|
||
IF enrollmentMap[course].size() > enrollmentMap[minCourse].size() THEN
|
||
minCourse ← course
|
||
END IF
|
||
END IF
|
||
END IF
|
||
END FOR
|
||
|
||
RETURN minCourse
|
||
END FUNCTION
|
||
```
|
||
|
||
**Why MRV?**
|
||
- Fail-fast on difficult variables
|
||
- Prunes search space early
|
||
- Empirically proven to reduce backtracking
|
||
|
||
---
|
||
|
||
#### ORDER_DOMAIN_VALUES (Least Constraining Value)
|
||
|
||
**Least Constraining Value:** Try values that leave most flexibility for remaining courses.
|
||
|
||
```
|
||
FUNCTION ORDER_DOMAIN_VALUES(course, domain, assignment, strategy):
|
||
// Calculate how constraining each value is
|
||
valueCounts ← {}
|
||
|
||
FOR EACH (classroom, day, slot) IN domain DO
|
||
// Count how many other courses can still use this {classroom, day, slot}
|
||
conflictCount ← countConflicts(classroom, day, slot, assignment)
|
||
valueCounts[(classroom, day, slot)] ← conflictCount
|
||
END FOR
|
||
|
||
// Sort by least constraining first
|
||
sortedValues ← SORT(domain, BY valueCounts, ASCENDING)
|
||
|
||
// Apply optimization strategy to break ties
|
||
strategyOrderedValues ← strategy.reorder(sortedValues)
|
||
|
||
RETURN strategyOrderedValues
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
#### CONSTRAINTS_SATISFIED
|
||
|
||
```
|
||
FUNCTION CONSTRAINTS_SATISFIED(course, classroom, day, slot, assignment, enrollmentMap):
|
||
// Constraint 1: Classroom capacity
|
||
IF enrollmentMap[course].size() > classroom.capacity THEN
|
||
RETURN FALSE // Already filtered in pre-processing, but double-check
|
||
END IF
|
||
|
||
// Constraint 2: No classroom double-booking
|
||
FOR EACH assignedCourse IN assignment.keys() DO
|
||
(c, d, s) ← assignment[assignedCourse]
|
||
IF c = classroom AND d = day AND s = slot THEN
|
||
RETURN FALSE // Classroom already used at this time
|
||
END IF
|
||
END FOR
|
||
|
||
// Constraint 3: No consecutive exams for any student
|
||
students ← enrollmentMap[course]
|
||
FOR EACH student IN students DO
|
||
IF hasConsecutiveExam(student, day, slot, assignment, enrollmentMap) THEN
|
||
RETURN FALSE
|
||
END IF
|
||
END FOR
|
||
|
||
// Constraint 4: Max 2 exams per day for any student
|
||
FOR EACH student IN students DO
|
||
examsOnDay ← countExamsForStudentOnDay(student, day, assignment, enrollmentMap)
|
||
IF examsOnDay >= 2 THEN
|
||
RETURN FALSE // Would cause student to have 3+ exams
|
||
END IF
|
||
END FOR
|
||
|
||
RETURN TRUE // All constraints satisfied
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
#### FORWARD_CHECK (Constraint Propagation)
|
||
|
||
**Purpose:** After assigning a course, reduce domains of unassigned courses by removing values that would violate constraints.
|
||
|
||
```
|
||
FUNCTION FORWARD_CHECK(assignedCourse, classroom, day, slot, assignment, domains, enrollmentMap):
|
||
removedValues ← {}
|
||
studentsInAssignedCourse ← enrollmentMap[assignedCourse]
|
||
|
||
FOR EACH unassignedCourse IN courses DO
|
||
IF unassignedCourse NOT IN assignment THEN
|
||
studentsInUnassignedCourse ← enrollmentMap[unassignedCourse]
|
||
sharedStudents ← INTERSECTION(studentsInAssignedCourse, studentsInUnassignedCourse)
|
||
|
||
IF sharedStudents.isNotEmpty() THEN
|
||
toRemove ← []
|
||
|
||
FOR EACH (c, d, s) IN domains[unassignedCourse] DO
|
||
// Remove if same classroom at same time
|
||
IF c = classroom AND d = day AND s = slot THEN
|
||
toRemove.add((c, d, s))
|
||
END IF
|
||
|
||
// Remove if consecutive slot for shared students
|
||
IF areConsecutive(day, slot, d, s) THEN
|
||
toRemove.add((c, d, s))
|
||
END IF
|
||
|
||
// Remove if would cause 3+ exams on same day for shared students
|
||
FOR EACH student IN sharedStudents DO
|
||
examsOnDay ← countExamsForStudentOnDay(student, d, assignment, enrollmentMap)
|
||
IF examsOnDay >= 2 AND d = day THEN
|
||
toRemove.add((c, d, s))
|
||
END IF
|
||
END FOR
|
||
END FOR
|
||
|
||
// Remove invalid values from domain
|
||
domains[unassignedCourse].removeAll(toRemove)
|
||
removedValues[unassignedCourse] ← toRemove
|
||
|
||
// Check if domain became empty (dead-end)
|
||
IF domains[unassignedCourse].isEmpty() THEN
|
||
removedValues.setEmptyDomainFlag(TRUE)
|
||
END IF
|
||
END IF
|
||
END IF
|
||
END FOR
|
||
|
||
RETURN removedValues
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
## CONSTRAINT VALIDATION
|
||
|
||
### Constraint 1: No Consecutive Exams
|
||
|
||
**Definition:** If a student has an exam in slot N, they cannot have another exam in slot N+1.
|
||
|
||
**Slot Numbering:**
|
||
|
||
```
|
||
Absolute Slot Number = (day - 1) × slotsPerDay + slot
|
||
|
||
Example (5 days, 4 slots per day):
|
||
Day 1, Slot 1 → Absolute slot 1
|
||
Day 1, Slot 2 → Absolute slot 2
|
||
Day 1, Slot 4 → Absolute slot 4
|
||
Day 2, Slot 1 → Absolute slot 5 ← Consecutive with Day 1, Slot 4!
|
||
Day 2, Slot 2 → Absolute slot 6
|
||
...
|
||
Day 5, Slot 4 → Absolute slot 20
|
||
```
|
||
|
||
**Implementation:**
|
||
|
||
```java
|
||
public boolean areConsecutive(int day1, int slot1, int day2, int slot2, int slotsPerDay) {
|
||
int absoluteSlot1 = (day1 - 1) * slotsPerDay + slot1;
|
||
int absoluteSlot2 = (day2 - 1) * slotsPerDay + slot2;
|
||
|
||
return Math.abs(absoluteSlot1 - absoluteSlot2) == 1;
|
||
}
|
||
|
||
public boolean hasConsecutiveExam(Student student, int day, int slot,
|
||
Assignment assignment, EnrollmentMap enrollmentMap) {
|
||
// Get all courses this student is enrolled in
|
||
List<Course> studentCourses = enrollmentMap.getCoursesForStudent(student);
|
||
|
||
for (Course course : studentCourses) {
|
||
if (assignment.contains(course)) {
|
||
ExamAssignment exam = assignment.get(course);
|
||
|
||
if (areConsecutive(day, slot, exam.day, exam.slot, config.slotsPerDay)) {
|
||
return true; // Found consecutive exam
|
||
}
|
||
}
|
||
}
|
||
|
||
return false; // No consecutive exams
|
||
}
|
||
```
|
||
|
||
**Edge Cases:**
|
||
- ✅ Day 1, Slot 4 and Day 2, Slot 1 ARE consecutive
|
||
- ✅ Day 1, Slot 1 has no previous slot (boundary)
|
||
- ✅ Day 5, Slot 4 has no next slot (boundary)
|
||
|
||
---
|
||
|
||
### Constraint 2: Max 2 Exams Per Day
|
||
|
||
**Definition:** A student cannot have more than 2 exams on any single day.
|
||
|
||
**Implementation:**
|
||
|
||
```java
|
||
public int countExamsForStudentOnDay(Student student, int day,
|
||
Assignment assignment, EnrollmentMap enrollmentMap) {
|
||
List<Course> studentCourses = enrollmentMap.getCoursesForStudent(student);
|
||
int count = 0;
|
||
|
||
for (Course course : studentCourses) {
|
||
if (assignment.contains(course)) {
|
||
ExamAssignment exam = assignment.get(course);
|
||
|
||
if (exam.day == day) {
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return count;
|
||
}
|
||
|
||
public boolean violatesMaxTwoPerDay(Student student, int day,
|
||
Assignment assignment, EnrollmentMap enrollmentMap) {
|
||
int currentCount = countExamsForStudentOnDay(student, day, assignment, enrollmentMap);
|
||
|
||
// Would cause 3+ exams if we add another exam on this day
|
||
return currentCount >= 2;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Constraint 3: Classroom Capacity
|
||
|
||
**Definition:** Number of students in a course cannot exceed classroom capacity.
|
||
|
||
**Implementation:**
|
||
|
||
```java
|
||
public boolean violatesCapacity(Course course, Classroom classroom, EnrollmentMap enrollmentMap) {
|
||
int enrolledCount = enrollmentMap.getStudentsForCourse(course).size();
|
||
|
||
return enrolledCount > classroom.getCapacity();
|
||
}
|
||
```
|
||
|
||
**Note:** This is pre-filtered during domain initialization, so should always be true during search.
|
||
|
||
---
|
||
|
||
### Constraint 4: No Classroom Double-Booking
|
||
|
||
**Definition:** A classroom cannot host two exams at the same time.
|
||
|
||
**Implementation:**
|
||
|
||
```java
|
||
public boolean isClassroomOccupied(Classroom classroom, int day, int slot, Assignment assignment) {
|
||
for (ExamAssignment exam : assignment.values()) {
|
||
if (exam.classroom.equals(classroom) && exam.day == day && exam.slot == slot) {
|
||
return true; // Classroom already used
|
||
}
|
||
}
|
||
|
||
return false; // Classroom available
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## HEURISTICS
|
||
|
||
### 1. Minimum Remaining Values (MRV)
|
||
|
||
**Purpose:** Variable ordering - choose which course to assign next
|
||
|
||
**Strategy:** Select course with fewest valid assignments remaining
|
||
|
||
**Why it works:**
|
||
- Courses with few options are harder to satisfy
|
||
- If we can't satisfy them, fail early (prune search tree)
|
||
- Empirically reduces backtracking by 10-100x
|
||
|
||
**Example:**
|
||
|
||
```
|
||
Unassigned courses:
|
||
- CourseCode_01: 50 valid assignments remaining
|
||
- CourseCode_02: 5 valid assignments remaining ← Choose this one (MRV)
|
||
- CourseCode_03: 30 valid assignments remaining
|
||
|
||
Reason: CourseCode_02 is most constrained, so try it first.
|
||
If it fails, we know early and can backtrack sooner.
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Least Constraining Value (LCV)
|
||
|
||
**Purpose:** Value ordering - which {classroom, day, slot} to try first
|
||
|
||
**Strategy:** Try values that eliminate fewest options for other courses
|
||
|
||
**Why it works:**
|
||
- Maximize flexibility for future assignments
|
||
- Reduce probability of backtracking
|
||
|
||
**Example:**
|
||
|
||
```
|
||
CourseCode_01 can be assigned to:
|
||
- (Classroom_01, Day 1, Slot 1) → Eliminates 10 options for other courses
|
||
- (Classroom_05, Day 3, Slot 2) → Eliminates 2 options for other courses ← Try this first (LCV)
|
||
|
||
Reason: Choosing Classroom_05 leaves more flexibility.
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Degree Heuristic (Tie-breaking for MRV)
|
||
|
||
**Purpose:** When multiple courses have same MRV, choose one with most conflicts
|
||
|
||
**Strategy:** Choose course enrolled by most students (more potential conflicts)
|
||
|
||
**Why it works:**
|
||
- Harder courses should be assigned earlier
|
||
- Similar to MRV (fail-fast on difficult variables)
|
||
|
||
**Example:**
|
||
|
||
```
|
||
Two courses both have 10 valid assignments remaining:
|
||
- CourseCode_01: 40 students enrolled ← Choose this (more conflicts)
|
||
- CourseCode_02: 15 students enrolled
|
||
|
||
Reason: CourseCode_01 affects more students, so constrain it first.
|
||
```
|
||
|
||
---
|
||
|
||
## OPTIMIZATION STRATEGIES
|
||
|
||
### Strategy 1: Minimize Days Used
|
||
|
||
**Goal:** Pack exams into as few days as possible
|
||
|
||
**Value Ordering Modification:**
|
||
|
||
```java
|
||
public int compareAssignments(Assignment a1, Assignment a2) {
|
||
// Prefer earlier days
|
||
if (a1.day != a2.day) {
|
||
return Integer.compare(a1.day, a2.day); // Ascending
|
||
}
|
||
|
||
// Within same day, prefer earlier slots
|
||
if (a1.slot != a2.slot) {
|
||
return Integer.compare(a1.slot, a2.slot); // Ascending
|
||
}
|
||
|
||
// Tie-break by classroom
|
||
return a1.classroom.compareTo(a2.classroom);
|
||
}
|
||
```
|
||
|
||
**Effect:** Tries to fill Day 1 completely before Day 2, etc.
|
||
|
||
**Use Case:** Shorter exam period, reduced facility costs
|
||
|
||
---
|
||
|
||
### Strategy 2: Balance Distribution Across Days
|
||
|
||
**Goal:** Spread exams evenly across all available days
|
||
|
||
**Value Ordering Modification:**
|
||
|
||
```java
|
||
public int compareAssignments(Assignment a1, Assignment a2) {
|
||
// Count how many exams already scheduled on each day
|
||
int count1 = countExamsOnDay(a1.day, currentAssignment);
|
||
int count2 = countExamsOnDay(a2.day, currentAssignment);
|
||
|
||
// Prefer day with fewer exams (balance)
|
||
if (count1 != count2) {
|
||
return Integer.compare(count1, count2); // Ascending
|
||
}
|
||
|
||
// Tie-break by slot
|
||
return Integer.compare(a1.slot, a2.slot);
|
||
}
|
||
```
|
||
|
||
**Effect:** Distributes exam load evenly
|
||
|
||
**Use Case:** Reduce student/proctor stress, balanced workload
|
||
|
||
---
|
||
|
||
### Strategy 3: Minimize Classrooms Needed
|
||
|
||
**Goal:** Use as few different classrooms as possible
|
||
|
||
**Value Ordering Modification:**
|
||
|
||
```java
|
||
public int compareAssignments(Assignment a1, Assignment a2) {
|
||
// Check if classroom already used
|
||
boolean a1Used = isClassroomUsed(a1.classroom, currentAssignment);
|
||
boolean a2Used = isClassroomUsed(a2.classroom, currentAssignment);
|
||
|
||
// Prefer already-used classrooms
|
||
if (a1Used && !a2Used) {
|
||
return -1; // a1 first
|
||
} else if (!a1Used && a2Used) {
|
||
return 1; // a2 first
|
||
}
|
||
|
||
// Both used or both unused: tie-break by day
|
||
return Integer.compare(a1.day, a2.day);
|
||
}
|
||
```
|
||
|
||
**Effect:** Reuses classrooms
|
||
|
||
**Use Case:** Centralize exam locations, reduce setup costs
|
||
|
||
---
|
||
|
||
### Strategy 4: Balance Classrooms Per Day
|
||
|
||
**Goal:** Use similar number of classrooms each day
|
||
|
||
**Value Ordering Modification:**
|
||
|
||
```java
|
||
public int compareAssignments(Assignment a1, Assignment a2) {
|
||
// Count unique classrooms used on each day
|
||
int classrooms1 = countUniqueClassroomsOnDay(a1.day, currentAssignment);
|
||
int classrooms2 = countUniqueClassroomsOnDay(a2.day, currentAssignment);
|
||
|
||
// Prefer day with fewer classrooms (balance)
|
||
if (classrooms1 != classrooms2) {
|
||
return Integer.compare(classrooms1, classrooms2);
|
||
}
|
||
|
||
// Tie-break by classroom
|
||
return a1.classroom.compareTo(a2.classroom);
|
||
}
|
||
```
|
||
|
||
**Effect:** Even classroom distribution across days
|
||
|
||
**Use Case:** Balance proctoring staff assignments
|
||
|
||
---
|
||
|
||
## CONFLICT DETECTION
|
||
|
||
### When Backtracking Fails
|
||
|
||
If backtracking returns FAILURE, it means no valid schedule exists. Analyze why:
|
||
|
||
```
|
||
FUNCTION ConflictDetector.analyze(courses, classrooms, enrollments, config):
|
||
conflicts ← ConflictReport()
|
||
|
||
// Check 1: Students with too many courses
|
||
FOR EACH student IN students DO
|
||
courseCount ← enrollments.getCoursesForStudent(student).size()
|
||
maxPossible ← config.numDays × 2 // Max 2 per day
|
||
|
||
IF courseCount > maxPossible THEN
|
||
conflicts.addStudentConflict(
|
||
student,
|
||
"Enrolled in " + courseCount + " courses, max possible " + maxPossible
|
||
)
|
||
END IF
|
||
END FOR
|
||
|
||
// Check 2: Courses with insufficient capacity
|
||
FOR EACH course IN courses DO
|
||
enrolledCount ← enrollments.getStudentsForCourse(course).size()
|
||
maxCapacity ← MAX(classrooms.capacity)
|
||
|
||
IF enrolledCount > maxCapacity THEN
|
||
conflicts.addCourseConflict(
|
||
course,
|
||
"Enrolled count " + enrolledCount + " exceeds max capacity " + maxCapacity
|
||
)
|
||
END IF
|
||
END FOR
|
||
|
||
// Check 3: Insufficient total slots
|
||
totalSlots ← config.numDays × config.slotsPerDay
|
||
IF courses.size() > totalSlots THEN
|
||
conflicts.addConfigurationConflict(
|
||
"Need " + courses.size() + " slots but only " + totalSlots + " available"
|
||
)
|
||
END IF
|
||
|
||
// Check 4: Overlapping enrollment patterns (complex graph analysis)
|
||
conflicts.addAll(detectEnrollmentCliques(enrollments))
|
||
|
||
// Generate recommendations
|
||
conflicts.setRecommendations(generateRecommendations(conflicts))
|
||
|
||
RETURN conflicts
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
### Recommendations
|
||
|
||
```
|
||
FUNCTION generateRecommendations(conflicts):
|
||
recommendations ← []
|
||
|
||
IF conflicts.hasStudentWithTooManyCourses() THEN
|
||
recommendations.add("Extend exam period to " + (requiredDays) + " days")
|
||
recommendations.add("Reduce course enrollments for affected students")
|
||
END IF
|
||
|
||
IF conflicts.hasCourseExceedingCapacity() THEN
|
||
recommendations.add("Add classroom with capacity ≥ " + (maxRequired))
|
||
recommendations.add("Split large course into multiple exam sessions")
|
||
END IF
|
||
|
||
IF conflicts.hasInsufficientSlots() THEN
|
||
recommendations.add("Increase slots per day to " + (requiredSlots))
|
||
recommendations.add("Extend exam period by " + (additionalDays) + " days")
|
||
END IF
|
||
|
||
RETURN recommendations
|
||
END FUNCTION
|
||
```
|
||
|
||
---
|
||
|
||
## PERFORMANCE OPTIMIZATIONS
|
||
|
||
### 1. Pre-compute Enrollment Maps
|
||
|
||
**Before backtracking:**
|
||
|
||
```java
|
||
// Build once, reuse throughout search
|
||
Map<Student, List<Course>> studentToCourses = new HashMap<>();
|
||
Map<Course, List<Student>> courseToStudents = new HashMap<>();
|
||
|
||
for (Enrollment e : enrollments) {
|
||
studentToCourses.computeIfAbsent(e.student, k -> new ArrayList<>()).add(e.course);
|
||
courseToStudents.computeIfAbsent(e.course, k -> new ArrayList<>()).add(e.student);
|
||
}
|
||
```
|
||
|
||
**Benefit:** O(1) lookup instead of O(n) database query
|
||
|
||
---
|
||
|
||
### 2. Domain Pre-filtering
|
||
|
||
**Before backtracking:**
|
||
|
||
```java
|
||
for (Course course : courses) {
|
||
int enrolledCount = courseToStudents.get(course).size();
|
||
|
||
// Remove classrooms that are too small
|
||
domain.get(course).removeIf(assignment ->
|
||
assignment.classroom.capacity < enrolledCount
|
||
);
|
||
}
|
||
```
|
||
|
||
**Benefit:** Reduces domain size by 50-90% typically
|
||
|
||
---
|
||
|
||
### 3. Memoization of Constraint Checks
|
||
|
||
**Cache repeated constraint checks:**
|
||
|
||
```java
|
||
Map<String, Boolean> constraintCache = new HashMap<>();
|
||
|
||
public boolean hasConsecutiveExam(Student student, int day, int slot) {
|
||
String key = student.id + ":" + day + ":" + slot;
|
||
|
||
if (constraintCache.containsKey(key)) {
|
||
return constraintCache.get(key); // O(1) cache hit
|
||
}
|
||
|
||
boolean result = computeConsecutiveExam(student, day, slot);
|
||
constraintCache.put(key, result);
|
||
return result;
|
||
}
|
||
```
|
||
|
||
**Benefit:** 10x faster constraint checking for repeated calls
|
||
|
||
---
|
||
|
||
### 4. Early Termination
|
||
|
||
**Stop as soon as solution found:**
|
||
|
||
```java
|
||
if (assignment.isComplete()) {
|
||
return assignment; // Don't search for other solutions
|
||
}
|
||
```
|
||
|
||
**Benefit:** Finds first valid solution quickly (don't need all solutions)
|
||
|
||
---
|
||
|
||
### 5. Parallel Domain Evaluation (Advanced)
|
||
|
||
**Use Java 8 streams for parallel constraint checking:**
|
||
|
||
```java
|
||
List<Assignment> validAssignments = domain.parallelStream()
|
||
.filter(a -> constraintsValidator.isValid(a, assignment, enrollmentMap))
|
||
.collect(Collectors.toList());
|
||
```
|
||
|
||
**Benefit:** Utilize multiple CPU cores, 2-4x speedup on multi-core machines
|
||
|
||
**Trade-off:** More complex, may not be needed for small datasets
|
||
|
||
---
|
||
|
||
## IMPLEMENTATION GUIDE
|
||
|
||
### Class Structure
|
||
|
||
```java
|
||
// Main solver class
|
||
public class CSPSolver {
|
||
private ConstraintValidator validator;
|
||
private OptimizationStrategy strategy;
|
||
private ProgressTracker progressTracker;
|
||
private volatile boolean cancelled = false;
|
||
|
||
public Schedule solve(
|
||
List<Course> courses,
|
||
List<Classroom> classrooms,
|
||
List<Enrollment> enrollments,
|
||
ScheduleConfiguration config,
|
||
OptimizationStrategy strategy,
|
||
ProgressTracker progressTracker
|
||
) throws NoSolutionException, CancelledException;
|
||
|
||
private Map<Course, ExamAssignment> backtrack(
|
||
Map<Course, ExamAssignment> assignment,
|
||
Map<Course, Set<AssignmentOption>> domains,
|
||
Map<Student, List<Course>> enrollmentMap
|
||
);
|
||
|
||
private Course selectUnassignedVariable(
|
||
Map<Course, ExamAssignment> assignment,
|
||
Map<Course, Set<AssignmentOption>> domains
|
||
);
|
||
|
||
private List<AssignmentOption> orderDomainValues(
|
||
Course course,
|
||
Set<AssignmentOption> domain,
|
||
Map<Course, ExamAssignment> assignment
|
||
);
|
||
|
||
private Map<Course, Set<AssignmentOption>> forwardCheck(
|
||
Course course,
|
||
AssignmentOption value,
|
||
Map<Course, ExamAssignment> assignment,
|
||
Map<Course, Set<AssignmentOption>> domains
|
||
);
|
||
|
||
public void cancel() {
|
||
this.cancelled = true;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Progress Tracking
|
||
|
||
```java
|
||
public interface ProgressTracker {
|
||
void updateProgress(int assignedCourses, int totalCourses);
|
||
void updateMessage(String message);
|
||
boolean isCancelled();
|
||
}
|
||
|
||
// Usage in solver:
|
||
private Map<Course, ExamAssignment> backtrack(...) {
|
||
int assignedCount = assignment.size();
|
||
int totalCourses = courses.size();
|
||
|
||
progressTracker.updateProgress(assignedCount, totalCourses);
|
||
|
||
if (progressTracker.isCancelled()) {
|
||
throw new CancelledException();
|
||
}
|
||
|
||
// Continue backtracking...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Integration with UI (JavaFX Task)
|
||
|
||
```java
|
||
public class ScheduleGenerationTask extends Task<Schedule> {
|
||
@Override
|
||
protected Schedule call() throws Exception {
|
||
updateMessage("Initializing solver...");
|
||
updateProgress(0, 100);
|
||
|
||
CSPSolver solver = new CSPSolver(validator, strategy);
|
||
|
||
ProgressTracker tracker = new ProgressTracker() {
|
||
@Override
|
||
public void updateProgress(int assigned, int total) {
|
||
double percentage = (double) assigned / total * 100;
|
||
ScheduleGenerationTask.this.updateProgress(assigned, total);
|
||
ScheduleGenerationTask.this.updateMessage(
|
||
"Assigned " + assigned + " / " + total + " courses"
|
||
);
|
||
}
|
||
|
||
@Override
|
||
public boolean isCancelled() {
|
||
return ScheduleGenerationTask.this.isCancelled();
|
||
}
|
||
};
|
||
|
||
Schedule schedule = solver.solve(courses, classrooms, enrollments, config, strategy, tracker);
|
||
|
||
return schedule;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## TESTING STRATEGY
|
||
|
||
### Unit Tests for Constraints
|
||
|
||
```java
|
||
@Test
|
||
public void testNoConsecutiveExams_WithConsecutiveSlots_ShouldReturnTrue() {
|
||
// Arrange
|
||
int day1 = 1, slot1 = 4;
|
||
int day2 = 2, slot2 = 1;
|
||
int slotsPerDay = 4;
|
||
|
||
// Act
|
||
boolean result = validator.areConsecutive(day1, slot1, day2, slot2, slotsPerDay);
|
||
|
||
// Assert
|
||
assertTrue(result, "Day 1 Slot 4 and Day 2 Slot 1 should be consecutive");
|
||
}
|
||
|
||
@Test
|
||
public void testMaxTwoPerDay_WithTwoExams_ShouldAllowThird() {
|
||
// Arrange
|
||
Student student = new Student("Std_001");
|
||
Assignment assignment = createAssignmentWithTwoExamsOnDay1(student);
|
||
|
||
// Act
|
||
boolean violates = validator.violatesMaxTwoPerDay(student, 1, assignment);
|
||
|
||
// Assert
|
||
assertTrue(violates, "Should violate max-2-per-day constraint");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Integration Tests for Solver
|
||
|
||
```java
|
||
@Test
|
||
public void testSolve_WithSolvableDataset_ShouldReturnValidSchedule() {
|
||
// Arrange
|
||
List<Student> students = TestDataBuilder.createStudents(10);
|
||
List<Course> courses = TestDataBuilder.createCourses(5);
|
||
List<Classroom> classrooms = TestDataBuilder.createClassrooms(3, 50);
|
||
List<Enrollment> enrollments = TestDataBuilder.createRandomEnrollments(students, courses);
|
||
ScheduleConfiguration config = new ScheduleConfiguration(3, 2); // 6 slots
|
||
|
||
CSPSolver solver = new CSPSolver(validator, new MinimizeDaysStrategy());
|
||
|
||
// Act
|
||
Schedule schedule = solver.solve(courses, classrooms, enrollments, config, strategy, null);
|
||
|
||
// Assert
|
||
assertNotNull(schedule);
|
||
assertEquals(5, schedule.getAssignments().size());
|
||
assertTrue(validator.validateSchedule(schedule));
|
||
}
|
||
|
||
@Test(expected = NoSolutionException.class)
|
||
public void testSolve_WithUnsolvableDataset_ShouldThrowException() {
|
||
// Arrange: Student enrolled in 10 courses, only 6 slots available
|
||
Student student = new Student("Std_001");
|
||
List<Course> courses = TestDataBuilder.createCourses(10);
|
||
List<Enrollment> enrollments = TestDataBuilder.enrollStudentInAllCourses(student, courses);
|
||
ScheduleConfiguration config = new ScheduleConfiguration(3, 2); // 6 slots, max 6 exams
|
||
|
||
CSPSolver solver = new CSPSolver(validator, strategy);
|
||
|
||
// Act & Assert
|
||
solver.solve(courses, classrooms, enrollments, config, strategy, null);
|
||
// Should throw NoSolutionException
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## COMPLEXITY ANALYSIS
|
||
|
||
### Time Complexity
|
||
|
||
**Worst case (no heuristics):**
|
||
```
|
||
O(d^n) where:
|
||
d = domain size (classrooms × days × slots)
|
||
n = number of courses
|
||
|
||
For sample data: O(200^20) ≈ 10^46 operations
|
||
```
|
||
|
||
**With MRV and LCV heuristics:**
|
||
```
|
||
Effective complexity: O(b^d) where:
|
||
b = effective branching factor (reduced by pruning)
|
||
d = depth of search tree
|
||
|
||
Empirically: ~O(n^2) to O(n^3) for solvable instances
|
||
```
|
||
|
||
**Forward checking:**
|
||
```
|
||
O(n^2 × d) per assignment where:
|
||
n = number of courses
|
||
d = domain size
|
||
|
||
But reduces search space exponentially, so net benefit.
|
||
```
|
||
|
||
---
|
||
|
||
### Space Complexity
|
||
|
||
```
|
||
O(n × d) where:
|
||
n = number of courses
|
||
d = domain size
|
||
|
||
Storage needed:
|
||
- Domains: n × d assignments
|
||
- Assignment: n course assignments
|
||
- Enrollment map: s × c (students × avg courses)
|
||
|
||
For sample data:
|
||
20 courses × 200 domain size = 4,000 assignment options
|
||
Memory: ~100 KB
|
||
```
|
||
|
||
---
|
||
|
||
### Performance Benchmarks
|
||
|
||
| Dataset | Courses | Students | Classrooms | Expected Time |
|
||
|---------|---------|----------|------------|---------------|
|
||
| Tiny | 5 | 50 | 3 | < 1 second |
|
||
| Small | 20 | 250 | 10 | < 5 seconds |
|
||
| Medium | 50 | 500 | 15 | < 10 seconds |
|
||
| Large | 100 | 1000 | 20 | < 60 seconds |
|
||
|
||
**Assumptions:** Well-distributed enrollments, solvable configuration
|
||
|
||
---
|
||
|
||
## VERSION HISTORY
|
||
|
||
| Version | Date | Author | Changes |
|
||
|---------|------|--------|---------|
|
||
| 1.0 | 2025-11-26 | Claude Code | Initial algorithm design |
|
||
|
||
---
|
||
|
||
## REFERENCES
|
||
|
||
- Russell & Norvig, "Artificial Intelligence: A Modern Approach", Chapter 6 (Constraint Satisfaction Problems)
|
||
- Bacchus & van Run, "Dynamic Variable Ordering in CSPs"
|
||
- Haralick & Elliott, "Increasing Tree Search Efficiency for CSPs"
|
||
- Online CSP Tutorial: https://artint.info/2e/html/ArtInt2e.Ch4.html
|
||
|
||
---
|
||
|
||
**END OF ALGORITHM DESIGN DOCUMENTATION**
|