#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
#include <queue>
#include <iomanip>
#include <fstream>
#include <cmath>
#include <map>

using namespace std;

struct Item {
    int id;
    int weight;
    int value;
    double density;
    int limit; // For multiple knapsack, -1 for complete

    Item(int id, int w, int v, int l = -1) : id(id), weight(w), value(v), limit(l) {
        density = (double)v / w;
    }
};

bool compareItems(const Item& a, const Item& b) {
    return a.density > b.density;
}

// Global random engine
mt19937 rng(42);

vector<Item> generate_items(int n, int max_w, int max_v, bool multiple = false, int max_k = 5) {
    vector<Item> items;
    uniform_int_distribution<int> w_dist(1, max_w);
    uniform_int_distribution<int> v_dist(1, max_v);
    uniform_int_distribution<int> k_dist(1, max_k);

    for (int i = 0; i < n; ++i) {
        int w = w_dist(rng);
        int v = v_dist(rng);
        int l = multiple ? k_dist(rng) : -1;
        items.emplace_back(i, w, v, l);
    }
    // Sort by density descending for BnB and DP consistency
    sort(items.begin(), items.end(), compareItems);
    return items;
}

// --- DP for True Value Calculation (Complete Knapsack) ---
// dp[i][w] = max value using items i...n-1 with capacity w
vector<vector<int>> compute_suffix_dp_complete(const vector<Item>& items, int capacity) {
    int n = items.size();
    vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));

    for (int i = n - 1; i >= 0; --i) {
        for (int w = 0; w <= capacity; ++w) {
            // Option 1: Don't take item i
            int res = dp[i + 1][w];
            // Option 2: Take at least 1 item i (if possible) -> then we are still at state i with w - weight
            if (w >= items[i].weight) {
                res = max(res, dp[i][w - items[i].weight] + items[i].value);
            }
            dp[i][w] = res;
        }
    }
    return dp;
}

// --- Cost Functions ---

// 1. Fractional Bound (Standard)
double bound_fractional(int level, int current_val, int rem_cap, const vector<Item>& items) {
    double bound = current_val;
    int w = rem_cap;
    for (int i = level; i < items.size(); ++i) {
        if (w >= items[i].weight) {
            // For complete knapsack, in fractional relaxation, we can take as many as fit...
            // Actually, complete knapsack fractional relaxation:
            // Just take the item with MAX density until full.
            // Since items are sorted, items[level] is the best available.
            // So we just fill the rest with items[level].
            bound += (double)w * items[i].density;
            return bound;
        } else {
             // Should not happen if we just take max density, 
             // but if we handle Multiple Knapsack, it's different.
             // For Complete Knapsack, sorted by density, just fill with the current best.
             bound += (double)w * items[i].density;
             return bound;
        }
    }
    return bound;
}

// 2. Simple Bound (Global Max Density) - A looser bound
double bound_simple(int level, int current_val, int rem_cap, double max_global_density) {
    return current_val + rem_cap * max_global_density;
}

// --- Branch and Bound Node ---
struct Node {
    int level; // Index of item being considered
    int current_val;
    int current_weight;
    double bound;
    
    // For analysis
    double true_opt; 

    bool operator<(const Node& other) const {
        return bound < other.bound; // Priority Queue is Max Heap by default? No, default is max. We want Max Bound first? Yes, exploring promising nodes.
    }
};

// --- BnB Solver for Complete Knapsack ---
struct BnBStats {
    int nodes_visited;
    long long time_us;
    int max_val;
    // Analysis data: Level -> List of (Bound, TrueVal)
    map<int, vector<pair<double, double>>> analysis_data; 
};

BnBStats solve_bnb_complete(const vector<Item>& items, int capacity, const vector<vector<int>>& dp_table, bool collect_analysis, int bound_mode = 0) {
    auto start_time = chrono::high_resolution_clock::now();
    BnBStats stats;
    stats.nodes_visited = 0;
    stats.max_val = 0;

    double max_global_density = 0;
    if (!items.empty()) max_global_density = items[0].density; // Since sorted

    priority_queue<Node> pq; 
    
    // Root node
    double init_bound;
    if (bound_mode == 1) init_bound = bound_simple(0, 0, capacity, max_global_density);
    else init_bound = bound_fractional(0, 0, capacity, items);

    pq.push({0, 0, 0, init_bound, (double)dp_table[0][capacity]});

    while (!pq.empty()) {
        Node u = pq.top();
        pq.pop();
        stats.nodes_visited++;

        if (u.bound <= stats.max_val) continue; // Prune

        if (collect_analysis) {
            stats.analysis_data[u.level].push_back({u.bound, u.true_opt});
        }

        if (u.level == items.size()) {
            if (u.current_val > stats.max_val) {
                stats.max_val = u.current_val;
            }
            continue;
        }

        // Branching for Complete Knapsack:
        // Commonly we branch on "count of item[level]". 
        // But infinite branching is hard for heap.
        // Alternative: Binary branching.
        // Left: Take 1 unit of item[level] (if fits). State: (level, w+wi, v+vi) -> Re-consider same item level.
        // Right: Don't take item[level] anymore. State: (level+1, w, v)
        
        // Let's use Binary Branching: Take 1 (Stay at level) vs Don't Take (Go to level + 1)
        
        // Child 1: Take item[level]
        if (u.current_weight + items[u.level].weight <= capacity) {
            int new_w = u.current_weight + items[u.level].weight;
            int new_v = u.current_val + items[u.level].value;
            int rem = capacity - new_w;
            
            double b;
            if (bound_mode == 1) b = bound_simple(u.level, new_v, rem, max_global_density);
            else b = bound_fractional(u.level, new_v, rem, items);

            // True val for child 1 (Still at u.level)
            double true_v = collect_analysis ? (new_v + dp_table[u.level][rem]) : 0;

            if (b > stats.max_val) {
                pq.push({u.level, new_v, new_w, b, true_v});
            }
        }

        // Child 2: Don't take item[level] (Move to next item)
        {
            int rem = capacity - u.current_weight;
            double b;
            // Bound calculation starts from level + 1
            if (bound_mode == 1) b = bound_simple(u.level + 1, u.current_val, rem, max_global_density);
            else b = bound_fractional(u.level + 1, u.current_val, rem, items);
            
            // True val for child 2 (at u.level + 1)
            double true_v = collect_analysis ? (u.current_val + dp_table[u.level + 1][rem]) : 0;

            if (b > stats.max_val) {
                pq.push({u.level + 1, u.current_val, u.current_weight, b, true_v});
            }
        }
    }

    auto end_time = chrono::high_resolution_clock::now();
    stats.time_us = chrono::duration_cast<chrono::microseconds>(end_time - start_time).count();
    return stats;
}

// --- Monte Carlo Estimation ---
long long monte_carlo_estimate(const vector<Item>& items, int capacity, int samples = 1000) {
    long long total_nodes = 0;
    
    for (int k = 0; k < samples; ++k) {
        long long path_nodes = 1; // Root
        int curr_w = 0;
        int level = 0;
        long long current_multiplier = 1;

        // Using the same binary branching structure: Take (stay) or Don't Take (next)
        while (level < items.size()) {
            vector<int> moves; // 0 = don't take, 1 = take
            
            // Can always "Don't take" -> move to level + 1
            moves.push_back(0);

            // Can "Take" if fits
            if (curr_w + items[level].weight <= capacity) {
                moves.push_back(1);
            }

            int branching_factor = moves.size();
            total_nodes += current_multiplier; // Add current level nodes count estimation

            if (branching_factor == 0) break; // Should not happen as 0 is always there

            // Pick random move
            int move = moves[rng() % branching_factor];
            
            current_multiplier *= branching_factor;

            if (move == 0) {
                level++; // Move to next item
            } else {
                curr_w += items[level].weight; // Take item, stay at level
            }
        }
    }
    
    return total_nodes / samples;
}

// --- Multiple Knapsack BnB ---
// Branching: Iterate k from 0 to limit[i]
// Cost Functions:
// 1. Fractional (Relaxed integer & limit constraint)
// 2. Simple (Global Max) - Not very useful for Multiple, let's use "Fractional with limits" vs "Fractional without limits" (Relaxed Limit)
// Wait, "Fractional Knapsack" normally respects limits in standard definition.
// Let's compare:
// MK Bound 1: Relaxed Integrity (Continuous 0..limit).
// MK Bound 2: Relaxed Integrity AND Relaxed Limit (Infinite count). (Looser)

double bound_mk_tight(int level, int current_val, int rem_cap, const vector<Item>& items) {
    double bound = current_val;
    int w = rem_cap;
    for (int i = level; i < items.size(); ++i) {
        int count = min(items[i].limit, w / items[i].weight); // Integer max count we could take?
        // No, fractional allows taking fraction of item.
        // Standard Fractional Knapsack with Limits:
        // Take as much as possible up to limit.
        if (items[i].weight == 0) continue; 
        
        int can_take_weight = items[i].limit * items[i].weight;
        
        if (w >= can_take_weight) {
            w -= can_take_weight;
            bound += items[i].value * items[i].limit;
        } else {
            bound += (double)w * items[i].density;
            return bound; // Filled
        }
    }
    return bound;
}

double bound_mk_loose(int level, int current_val, int rem_cap, const vector<Item>& items) {
    // Like Complete Knapsack - ignore limits
    double bound = current_val;
    int w = rem_cap;
    for (int i = level; i < items.size(); ++i) {
        if (w >= items[i].weight) {
             bound += (double)w * items[i].density;
             return bound;
        }
    }
    return bound;
}

struct NodeMK {
    int level; 
    int current_val;
    int current_weight;
    double bound;
    
    bool operator<(const NodeMK& other) const {
        return bound < other.bound; 
    }
};

BnBStats solve_bnb_mk(const vector<Item>& items, int capacity, int bound_mode) {
    auto start_time = chrono::high_resolution_clock::now();
    BnBStats stats;
    stats.nodes_visited = 0;
    stats.max_val = 0;

    priority_queue<NodeMK> pq;
    double init_bound = (bound_mode == 0) ? bound_mk_tight(0, 0, capacity, items) : bound_mk_loose(0, 0, capacity, items);
    
    pq.push({0, 0, 0, init_bound});

    while (!pq.empty()) {
        NodeMK u = pq.top();
        pq.pop();
        stats.nodes_visited++;

        if (u.bound <= stats.max_val) continue;

        if (u.level == items.size()) {
            if (u.current_val > stats.max_val) {
                stats.max_val = u.current_val;
            }
            continue;
        }

        // Branching: 0 to limit
        // To be safe and simple, let's branch on specific counts? 
        // Or Binary: Take 1 (decrement limit) vs Don't Take (next level).
        // Let's use Binary with state modification (decrement limit).
        // Wait, Node needs to store remaining limits? That's too heavy.
        // Standard MK branching: Loop k = 0 to Limit? That's a high branching factor.
        // Binary branching is better:
        // Option 1: Take 1 of Item[level]. (Requires tracking 'used' count for current level).
        // Option 2: Don't take any more of Item[level]. (Move to level+1).
        
        // We need to store 'count_taken_current_level' in Node? 
        // Or just simplify: "level" implies items[level].
        // But we need to know how many we already took.
        // Let's assume standard MK backtracking logic where we just loop k.
        // But for Priority Queue, creating k children is fine.
        
        int k_max = min(items[u.level].limit, (capacity - u.current_weight) / items[u.level].weight);
        
        // Generate children for k = 0 to k_max
        // Heuristic: Try largest k first? Priority Queue handles order.
        
        for (int k = 0; k <= k_max; ++k) {
            int new_w = u.current_weight + k * items[u.level].weight;
            int new_v = u.current_val + k * items[u.level].value;
            int rem = capacity - new_w;
            
            double b;
            if (bound_mode == 0) b = bound_mk_tight(u.level + 1, new_v, rem, items);
            else b = bound_mk_loose(u.level + 1, new_v, rem, items);
            
            if (b > stats.max_val) {
                pq.push({u.level + 1, new_v, new_w, b});
            }
        }
    }

    auto end_time = chrono::high_resolution_clock::now();
    stats.time_us = chrono::duration_cast<chrono::microseconds>(end_time - start_time).count();
    return stats;
}


int main() {
    int W = 100;
    
    // --- Experiment 1: Monte Carlo Estimation ---
    ofstream mc_file("backtracking/mc_estimation.csv");
    mc_file << "n,estimated_nodes\n";
    cout << "Running Monte Carlo Estimation..." << endl;
    for (int n = 5; n <= 25; n += 5) {
        auto items = generate_items(n, 40, 100);
        long long est = monte_carlo_estimate(items, W, 2000);
        mc_file << n << "," << est << "\n";
    }
    mc_file.close();

    // --- Experiment 2 & 3: Cost Function Analysis (Fixed n & Varying n) ---
    // We need: Level, Cost, TrueVal
    // Also "Approximation Effect at different input sizes" -> Avg Error vs n
    
    ofstream cost_level_file("backtracking/cost_approx_level.csv");
    cost_level_file << "n,level,cost,true_val,ratio\n";
    
    ofstream cost_n_file("backtracking/cost_approx_n.csv");
    cost_n_file << "n,avg_ratio\n";
    
    cout << "Running Cost Function Analysis..." << endl;
    for (int n = 5; n <= 25; n += 5) {
        double total_ratio_sum = 0;
        long long total_nodes_count = 0;

        // Run multiple samples
        for (int s = 0; s < 5; ++s) {
            auto items = generate_items(n, 40, 100);
            auto dp = compute_suffix_dp_complete(items, W);
            
            // Collect analysis data using Standard Bound (Mode 0)
            auto res = solve_bnb_complete(items, W, dp, true, 0);
            
            for (auto const& [lvl, data] : res.analysis_data) {
                for (auto const& pair : data) {
                    double cost = pair.first;
                    double true_v = pair.second;
                    double ratio = (true_v > 0) ? (cost / true_v) : 1.0;
                    
                    if (s == 0) { // Only log details for one sample to avoid huge files
                        cost_level_file << n << "," << lvl << "," << cost << "," << true_v << "," << ratio << "\n";
                    }
                    
                    total_ratio_sum += ratio;
                    total_nodes_count++;
                }
            }
        }
        if (total_nodes_count > 0)
             cost_n_file << n << "," << (total_ratio_sum / total_nodes_count) << "\n";
    }
    cost_level_file.close();
    cost_n_file.close();

    // --- Experiment 4: New Cost Function Analysis ---
    // Compare Mode 0 (Fractional - Tight) vs Mode 1 (Simple - Loose)
    ofstream new_cost_file("backtracking/new_cost_analysis.csv");
    new_cost_file << "n,method,nodes,time_us\n";
    
    cout << "Running New Cost Function Analysis..." << endl;
    for (int n = 5; n <= 25; n += 5) {
        long long nodes0 = 0, time0 = 0;
        long long nodes1 = 0, time1 = 0;
        int runs = 10;
        
        for (int r = 0; r < runs; ++r) {
            auto items = generate_items(n, 40, 100);
            auto dp = compute_suffix_dp_complete(items, W); // Just for empty passing
            
            auto res0 = solve_bnb_complete(items, W, dp, false, 0); // Fractional
            nodes0 += res0.nodes_visited;
            time0 += res0.time_us;
            
            auto res1 = solve_bnb_complete(items, W, dp, false, 1); // Simple
            nodes1 += res1.nodes_visited;
            time1 += res1.time_us;
        }
        
        new_cost_file << n << ",Fractional," << nodes0/runs << "," << time0/runs << "\n";
        new_cost_file << n << ",Simple," << nodes1/runs << "," << time1/runs << "\n";
    }
    new_cost_file.close();

    // --- Experiment 5: Multiple Knapsack (Additional) ---
    ofstream mk_file("backtracking/multiple_knapsack.csv");
    mk_file << "n,method,nodes,time_us\n";
    
    cout << "Running Multiple Knapsack Analysis..." << endl;
    for (int n = 5; n <= 20; n += 5) { // Smaller range as it might be slower
        long long nodes0 = 0, time0 = 0; // Tight
        long long nodes1 = 0, time1 = 0; // Loose
        int runs = 10;
        
        for (int r = 0; r < runs; ++r) {
            auto items = generate_items(n, 40, 100, true, 3); // Limit up to 3
            
            auto res0 = solve_bnb_mk(items, W, 0); 
            nodes0 += res0.nodes_visited;
            time0 += res0.time_us;
            
            auto res1 = solve_bnb_mk(items, W, 1);
            nodes1 += res1.nodes_visited;
            time1 += res1.time_us;
        }
        mk_file << n << ",TightBound," << nodes0/runs << "," << time0/runs << "\n";
        mk_file << n << ",LooseBound," << nodes1/runs << "," << time1/runs << "\n";
    }
    mk_file.close();

    return 0;
}
