Programming Projects

Simulated Annealing Schedule Optimizer

Mar 2025

Advanced metaheuristic optimization using Simulated Annealing to assign orders to 20 students while maximizing profit, respecting delivery time windows and student eligibility constraints. Complex real-world logistics problem solving with sophisticated algorithmic approaches.

Simulated Annealing Metaheuristic Optimization Local Search Combinatorial Optimization Logistics
Final Profit
€8,974
Student Consulting Services
Improvement
+66.8%
From €5,379 baseline
Iterations
18M
In 150 seconds
Acceptance Ratio
0.89
High exploration rate
🎯 Problem Overview

Student Consulting Services B.V needs to assign delivery orders to 20 consultants to maximize profit. Constraints: (1) Max shift time: 8 hours per student, (2) Orders have location-dependent driving times, (3) Only eligible students can take specific orders, (4) Profit = order value - driving + work time. Initial solutions yield €5,000-5,500. Goal: optimize using Simulated Annealing.

🧠 Algorithm & Approach

Local Search Operators (4 types):
Insert Order (35%) — Add unassigned order to student's route if feasible
Remove Order (10%) — Remove assigned order for reassignment
Move Order (20%) — Transfer order between students
Two-Opt Swap (35%) — Reverse route segment to minimize driving time

Cooling Schedule: Initial temperature 1,000,000 → cooling rate 0.999999 (very gradual) ensures extensive exploration of solution space before convergence. Temperature-dependent acceptance probability prevents getting stuck in local optima.

📊 Performance Results

Initial experiments: Cooling rate 0.01 yielded only €5,379
Tuned parameters: Cooling rate 0.999999 achieved €8,974.05
Improvement: 66.8% gain through slower cooling (better exploration)
Verification: 121,500 iterations/second × 150 seconds = 18M iterations total. All constraints satisfied; solution verified as valid.

SimulatedAnnealingSolver.java
import java.util.*;
import java.io.File;
import java.io.PrintWriter;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;

public class SimulatedAnnealingSolver {

    // Constants
    private static final int MAX_SHIFT_TIME = 8 * 60 * 60;
    private static final int HQ_NODE = 251;
    private static final int NUM_STUDENTS = 20;
    
    // Data structures
    private static int[][] drivingTimes;
    private static Order[] orders;
    private static int numOrders;
    private static Solution bestSolution;
    
    // Parameters for SA
    private static final double INITIAL_TEMPERATURE = 1000000;
    private static final double MIN_TEMPERATURE = 1e-8;
    private static final double COOLING_RATE = 0.999999;
    private static final int MAX_RUNTIME_SECONDS = 150;
    
    // Tunable parameters for operator selection
    private static final double[] OPERATOR_WEIGHTS = {0.35, 0.1, 0.2, 0.35}; 
    private static double[] cumulativeWeights;
    
    // Statistics tracking
    private static long acceptedMoves = 0;
    private static long rejectedMoves = 0;
    
    static class Order {
        int id, node, duration, profit;
        boolean[] eligibleStudents;
        int[] drivingTimesFromHQ;
        int[] drivingTimesToHQ;
        
        Order(int id, int node, int duration, int profit, boolean[] eligibleStudents) {
            this.id = id;
            this.node = node;
            this.duration = duration;
            this.profit = profit;
            this.eligibleStudents = eligibleStudents;
        }
        
        void precomputeDrivingTimes() {
            drivingTimesFromHQ = new int[numOrders + 1];
            drivingTimesToHQ = new int[numOrders + 1];
            drivingTimesFromHQ[numOrders] = drivingTimes[HQ_NODE][node];
            drivingTimesToHQ[numOrders] = drivingTimes[node][HQ_NODE];
            
            for (int i = 0; i < numOrders; i++) {
                drivingTimesFromHQ[i] = drivingTimes[HQ_NODE][orders[i].node];
                drivingTimesToHQ[i] = drivingTimes[orders[i].node][HQ_NODE];
            }
        }
    }
    
    static class Solution {
        List<Integer>[] routes;
        Set<Integer> unassignedOrders;
        double profit;
        
        int[] routeTimes;
        int[] routeDrivingTimes;
        int[] routeWorkTimes;
        int[] routeProfits;
        
        @SuppressWarnings("unchecked")
        public Solution() {
            routes = new ArrayList[NUM_STUDENTS];
            for (int i = 0; i < NUM_STUDENTS; i++) {
                routes[i] = new ArrayList<>();
            }
            unassignedOrders = new HashSet<>();
            routeTimes = new int[NUM_STUDENTS];
            routeDrivingTimes = new int[NUM_STUDENTS];
            routeWorkTimes = new int[NUM_STUDENTS];
            routeProfits = new int[NUM_STUDENTS];
            profit = 0;
        }
        
        public Solution copy() {
            Solution copy = new Solution();
            for (int i = 0; i < NUM_STUDENTS; i++) {
                copy.routes[i] = new ArrayList<>(this.routes[i]);
                copy.routeTimes[i] = this.routeTimes[i];
                copy.routeDrivingTimes[i] = this.routeDrivingTimes[i];
                copy.routeWorkTimes[i] = this.routeWorkTimes[i];
                copy.routeProfits[i] = this.routeProfits[i];
            }
            copy.unassignedOrders = new HashSet<>(this.unassignedOrders);
            copy.profit = this.profit;
            return copy;
        }
        
        private void calculateRouteMetrics(int student) {
            int drivingTime = 0;
            int workTime = 0;
            int routeProfit = 0;
            
            List<Integer> route = routes[student];
            if (route.isEmpty()) {
                routeDrivingTimes[student] = 0;
                routeWorkTimes[student] = 0;
                routeProfits[student] = 0;
                routeTimes[student] = 0;
                return;
            }
            
            int currentNode = HQ_NODE;
            for (int i = 0; i < route.size(); i++) {
                int orderIndex = route.get(i);
                Order order = orders[orderIndex];
                
                drivingTime += drivingTimes[currentNode][order.node];
                workTime += order.duration;
                routeProfit += order.profit;
                currentNode = order.node;
            }
            
            drivingTime += drivingTimes[currentNode][HQ_NODE];
            
            routeDrivingTimes[student] = drivingTime;
            routeWorkTimes[student] = workTime;
            routeProfits[student] = routeProfit;
            routeTimes[student] = drivingTime + workTime;
        }
        
        public void recalculateAllMetrics() {
            profit = 0;
            for (int i = 0; i < NUM_STUDENTS; i++) {
                calculateRouteMetrics(i);
                profit += routeProfits[i] - routeTimes[i];
            }
        }
        
        public void updateMetricsAfterInsert(int student, int orderIndex, int position) {
            calculateRouteMetrics(student);
            profit = 0;
            for (int i = 0; i < NUM_STUDENTS; i++) {
                profit += routeProfits[i] - routeTimes[i];
            }
        }
        
        public void updateMetricsAfterRemove(int student) {
            calculateRouteMetrics(student);
            profit = 0;
            for (int i = 0; i < NUM_STUDENTS; i++) {
                profit += routeProfits[i] - routeTimes[i];
            }
        }
        
        public boolean isRouteFeasible(int student) {
            return routeTimes[student] <= MAX_SHIFT_TIME;
        }
        
        public boolean verify() {
            for (int i = 0; i < NUM_STUDENTS; i++) {
                if (routeTimes[i] > MAX_SHIFT_TIME) {
                    return false;
                }
            }
            
            boolean[] assigned = new boolean[numOrders];
            for (int i = 0; i < NUM_STUDENTS; i++) {
                for (int orderIndex : routes[i]) {
                    if (assigned[orderIndex]) return false;
                    assigned[orderIndex] = true;
                }
            }
            
            for (int i = 0; i < NUM_STUDENTS; i++) {
                for (int orderIndex : routes[i]) {
                    if (!orders[orderIndex].eligibleStudents[i]) return false;
                }
            }
            
            return true;
        }
    }
    
    public static void main(String[] args) throws Exception {
        readDrivingTimes("drivingtimes.txt");
        readOrders("orders.txt");
        precomputeOperatorWeights();
        
        Solution currentSolution = initializeSolution();
        currentSolution.recalculateAllMetrics();
        
        bestSolution = currentSolution.copy();
        double currentProfit = currentSolution.profit;
        double bestProfit = bestSolution.profit;
        
        double temperature = INITIAL_TEMPERATURE;
        long startTime = System.currentTimeMillis();
        long endTime = startTime + MAX_RUNTIME_SECONDS * 1000;
        int iterations = 0;
        
        ThreadLocalRandom random = ThreadLocalRandom.current();
        System.out.println("Starting simulated annealing...");
        
        while (System.currentTimeMillis() < endTime) {
            iterations++;
            
            Solution neighborSolution = applyRandomOperator(currentSolution);
            double delta = neighborSolution.profit - currentProfit;
            
            boolean accept = false;
            if (delta > 0) {
                accept = true;
            } else if (temperature > 0) {
                double acceptanceProbability = Math.exp(delta / temperature);
                accept = random.nextDouble() < acceptanceProbability;
            }
            
            if (accept) {
                acceptedMoves++;
                currentSolution = neighborSolution;
                currentProfit = neighborProfit;
                
                if (currentProfit > bestProfit) {
                    bestSolution = currentSolution.copy();
                    bestProfit = currentProfit;
                }
            } else {
                rejectedMoves++;
            }
            
            temperature *= COOLING_RATE;
        }
        
        System.out.println("Simulated annealing completed:");
        System.out.println("Total iterations: " + iterations);
        System.out.println("Best profit: " + (bestProfit / 60.0) + " euros");
        System.out.println("Solution valid: " + bestSolution.verify());
    }
}
University Assignments

A7 — Profitable Printing

Mar 2024

Dynamic programming for optimal book partition. Determine how to split n chapters into contiguous books to maximize total profit, where each book partition has a predetermined profit value P[i,j].

Dynamic Programming Optimization Modelling & Computing
Problem

A printing company publishes a story with n chapters. Publishing chapters i...j as one book yields profit P[i,j]. Find the partitioning that maximizes total profit.

ProfitablePrinting.java
public static int calculateMaxProfit(int[][] profit, int n) {
    int[] dp = new int[n + 1];
    dp[0] = 0;
    
    // Dynamic programming loop
    for (int i = 1; i <= n; i++) {
        // Consider alternatives for the last book
        for (int j = 1; j <= i; j++) {
            // j = first chapter in the last book (chapters j...i)
            // Total profit = profit from chapters 1...j-1 + profit from book j...i
            dp[i] = Math.max(dp[i], dp[j - 1] + profit[j][i]);
        }
    }
    
    return dp[n];
}

A5 — Space Tourism

Feb 2024

0/1 Knapsack problem for spacecraft payload optimization. Select tourists to maximize revenue while respecting weight capacity constraints.

Knapsack Problem Greedy Algorithm Modelling & Computing
SpaceTourism.java
// Greedy approach: select lightest passengers first
Arrays.sort(tourists);

int totalRevenue = 0;
int currentWeight = 0;
List<Integer> selectedPassengers = new ArrayList<>();

for (Tourist t : tourists) {
    if (currentWeight + t.weight <= M) {
        currentWeight += t.weight;
        totalRevenue += t.price;
        selectedPassengers.add(t.id);
    }
}

B4 — Project Delays

Feb 2024

Topological sort with task scheduling. Find the minimum completion time for all tasks given their durations and dependencies using Kahn's algorithm.

Graph Theory Topological Sort Modelling & Computing
B4Exercise.java (excerpt)
private static int minimumCompletionTime(Task[] tasks, 
        List<Integer> completionOrder, int n) {
    Queue<Integer> queue = new LinkedList<>();
    int[] taskCompletionTime = new int[n + 1];
    
    // Kahn's algorithm: start with tasks having no prerequisites
    for (int i = 1; i <= n; i++) {
        if (tasks[i].prerequisites == 0) {
            queue.add(i);
            taskCompletionTime[i] = tasks[i].duration;
        }
    }
    
    int countCompleted = 0;
    while (!queue.isEmpty()) {
        int currentTask = queue.poll();
        countCompleted++;
        completionOrder.add(currentTask);
        
        for (int nextTask : tasks[currentTask].nextTasks) {
            taskCompletionTime[nextTask] = Math.max(
                taskCompletionTime[nextTask], 
                taskCompletionTime[currentTask] + tasks[nextTask].duration);
            tasks[nextTask].prerequisites--;
            if (tasks[nextTask].prerequisites == 0) {
                queue.add(nextTask);
            }
        }
    }
    
    if (countCompleted != n) return -1;
    return Arrays.stream(taskCompletionTime).max().getAsInt();
}