Files
Se302/ALGORITHM_DESIGN.md

40 KiB
Raw Blame History

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
  2. Problem Formulation
  3. Algorithm Architecture
  4. Backtracking Algorithm
  5. Constraint Validation
  6. Heuristics
  7. Optimization Strategies
  8. Conflict Detection
  9. Performance Optimizations
  10. Implementation Guide
  11. Testing Strategy
  12. 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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

// 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:

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:

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:

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:

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

// 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

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)

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

@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

@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