#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <fstream>
#include <random>
#include <chrono>
#include <iomanip>
#include <climits>

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<Job>& jobs) {
    if (jobs.empty()) return 0;
    vector<long long> 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<Job> 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<long long> 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<Job> G_jobs;
vector<long long> 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<Job> jobs) {
    if (jobs.empty()) return 0;
    
    // Heuristic: LPT gives a good initial bound
    vector<Job> 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<Job> generate_jobs(int n, int min_val, int max_val) {
    vector<Job> 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<int> ms = {3, 5, 8};
    vector<int> 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<Job> 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<double, micro>(t6 - t5).count();
                }
                
                double ls_dur = chrono::duration<double, micro>(t2 - t1).count();
                double lpt_dur = chrono::duration<double, micro>(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<int> test_ms = {3, 4, 5};
    for (int m : test_ms) {
        vector<Job> jobs;
        int num_small = m * (m - 1);
        for(int i=0; i<num_small; ++i) jobs.push_back({i, 1});
        jobs.push_back({num_small, (long long)m});
        
        long long res_ls = greedy_ls(m, jobs);
        long long res_opt = solve_optimal(m, jobs);
        double ratio = (double)res_ls / res_opt;
        double bound = 2.0 - 1.0/m;
        
        out << "LS_Worst," << m << "," << jobs.size() << ","
            << "\"m*(m-1) 1s + one m\"" << ","
            << res_ls << "," << res_opt << "," << ratio << "," << bound << "\n";
    }

    // Case 2: LPT Worst Case
    // Known example: m=2, Jobs {3, 3, 2, 2, 2}
    // LPT: M1[3, 2, 2] (7), M2[3, 2] (5). Max 7.
    // Opt: M1[3, 3] (6), M2[2, 2, 2] (6). Max 6.
    // Ratio 7/6 = 1.1666. Theory 4/3 - 1/(3m) = 1.33 - 0.166 = 1.166.
    {
        int m = 2;
        vector<Job> 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;
}
