#include #include #include #include #include #include #include #include #include #include using namespace std; // --- Data Structures --- struct Job { int id; long long duration; }; struct ExperimentResult { int n; int m; long long greedy_ls_makespan; long long greedy_lpt_makespan; long long optimal_makespan; // -1 if failed double ls_time_us; double lpt_time_us; double opt_time_us; }; // --- Algorithms --- // Greedy 1: List Scheduling (Arbitrary/Online) long long greedy_ls(int m, const vector& jobs) { if (jobs.empty()) return 0; vector machines(m, 0); for (const auto& job : jobs) { // Find machine with min load int min_idx = 0; for (int i = 1; i < m; ++i) { if (machines[i] < machines[min_idx]) { min_idx = i; } } machines[min_idx] += job.duration; } return *max_element(machines.begin(), machines.end()); } // Greedy 2: LPT (Longest Processing Time) long long greedy_lpt(int m, vector jobs) { // Note: pass by value to sort copy if (jobs.empty()) return 0; sort(jobs.begin(), jobs.end(), [](const Job& a, const Job& b) { return a.duration > b.duration; }); vector machines(m, 0); // Optimization: Use a min-priority queue if m is large, but for small m linear scan is fine/faster due to cache for (const auto& job : jobs) { int min_idx = 0; for (int i = 1; i < m; ++i) { if (machines[i] < machines[min_idx]) { min_idx = i; } } machines[min_idx] += job.duration; } return *max_element(machines.begin(), machines.end()); } // Optimal Solver: Branch and Bound // Global variables for recursion to avoid passing too many args long long best_makespan; int G_m; vector G_jobs; vector G_machines; long long start_time_opt; bool time_out; void dfs(int job_idx, long long current_max) { if (time_out) return; // Check timeout (e.g., 100ms per instance for batch tests) if ((clock() - start_time_opt) / CLOCKS_PER_SEC > 1.0) { // 1 second timeout time_out = true; return; } // Pruning 1: If current max load >= best solution found so far, prune if (current_max >= best_makespan) return; // Base case: all jobs assigned if (job_idx == G_jobs.size()) { best_makespan = current_max; return; } // Pruning 2: Theoretical lower bound // If (sum of remaining jobs + current total load) / m > best_makespan, prune? // A simpler bound: max(current_max, (sum of remaining + sum of current loads) / m) // Calculating sum every time is slow, can be optimized. long long job_len = G_jobs[job_idx].duration; // Try to assign job to each machine for (int i = 0; i < G_m; ++i) { // Optimization: Symmetry breaking // If this machine has same load as previous machine, and we tried previous, skip this one. // This assumes machines are initially 0. // A simpler symmetry break: if machines[i] == machines[i-1] (and they are interchangeable), skip. // Requires machines to be sorted or checked. // For now, simpler check: if this is the first empty machine, stop after trying it. if (G_machines[i] == 0) { G_machines[i] += job_len; dfs(job_idx + 1, max(current_max, G_machines[i])); G_machines[i] -= job_len; break; // Don't try other empty machines } if (G_machines[i] + job_len < best_makespan) { G_machines[i] += job_len; dfs(job_idx + 1, max(current_max, G_machines[i])); G_machines[i] -= job_len; } } } long long solve_optimal(int m, vector jobs) { if (jobs.empty()) return 0; // Heuristic: LPT gives a good initial bound vector sorted_jobs = jobs; sort(sorted_jobs.begin(), sorted_jobs.end(), [](const Job& a, const Job& b) { return a.duration > b.duration; }); best_makespan = greedy_lpt(m, sorted_jobs); G_m = m; G_jobs = sorted_jobs; G_machines.assign(m, 0); time_out = false; start_time_opt = clock(); dfs(0, 0); if (time_out) return -1; return best_makespan; } // --- Test Generation --- vector generate_jobs(int n, int min_val, int max_val) { vector jobs(n); random_device rd; mt19937 gen(rd()); uniform_int_distribution<> dis(min_val, max_val); for (int i = 0; i < n; ++i) { jobs[i] = {i, (long long)dis(gen)}; } return jobs; } // --- Main Experiments --- void run_experiments() { ofstream out("results/algo_comparison.csv"); out << "m,n,ls_makespan,lpt_makespan,opt_makespan,ls_time,lpt_time,opt_time,ls_ratio,lpt_ratio\n"; cout << "Running random experiments..." << endl; vector ms = {3, 5, 8}; vector ns = {10, 15, 20, 25, 30, 50, 100}; for (int m : ms) { for (int n : ns) { int runs = 10; // More runs for faster algos if (n > 20) runs = 20; for (int r = 0; r < runs; ++r) { vector jobs = generate_jobs(n, 10, 100); auto t1 = chrono::high_resolution_clock::now(); long long ls_res = greedy_ls(m, jobs); auto t2 = chrono::high_resolution_clock::now(); auto t3 = chrono::high_resolution_clock::now(); long long lpt_res = greedy_lpt(m, jobs); auto t4 = chrono::high_resolution_clock::now(); long long opt_res = -1; double opt_dur = 0; // Only run optimal for small n if (n <= 18) { auto t5 = chrono::high_resolution_clock::now(); opt_res = solve_optimal(m, jobs); auto t6 = chrono::high_resolution_clock::now(); opt_dur = chrono::duration(t6 - t5).count(); } double ls_dur = chrono::duration(t2 - t1).count(); double lpt_dur = chrono::duration(t4 - t3).count(); double ls_ratio = (opt_res != -1 && opt_res != 0) ? (double)ls_res / opt_res : -1.0; double lpt_ratio = (opt_res != -1 && opt_res != 0) ? (double)lpt_res / opt_res : -1.0; out << m << "," << n << "," << ls_res << "," << lpt_res << "," << opt_res << "," << ls_dur << "," << lpt_dur << "," << opt_dur << "," << ls_ratio << "," << lpt_ratio << "\n"; } } } out.close(); cout << "Experiments complete. Results saved." << endl; } void verify_worst_cases() { ofstream out("results/worst_case_verification.csv"); out << "case_type,m,n,input_desc,greedy_res,opt_res,ratio,theory_bound\n"; // Case 1: LS Worst Case // m machines. Input: m*(m-1) jobs of size 1, then 1 job of size m. // Example m=3. 6 jobs of size 1, 1 job of size 3. // Greedy: [1,1,3], [1,1], [1,1] -> Max 5. // Opt: [3], [1,1,1], [1,1,1] -> Max 3. // Ratio 5/3 approx 1.666. Theory 2 - 1/3 = 1.666. vector test_ms = {3, 4, 5}; for (int m : test_ms) { vector jobs; int num_small = m * (m - 1); for(int i=0; i jobs = { {0,3}, {1,3}, {2,2}, {3,2}, {4,2} }; long long res_lpt = greedy_lpt(m, jobs); long long res_opt = solve_optimal(m, jobs); // Should be fast double ratio = (double)res_lpt / res_opt; double bound = 4.0/3.0 - 1.0/(3.0*m); out << "LPT_Worst," << m << "," << jobs.size() << "," << "\"{3,3,2,2,2}\"" << "," << res_lpt << "," << res_opt << "," << ratio << "," << bound << "\n"; } out.close(); cout << "Worst case verification complete." << endl; } int main() { verify_worst_cases(); run_experiments(); return 0; }