diff --git a/docs/challenges/knapsack.md b/docs/challenges/knapsack.md index cecb097..be7e71d 100644 --- a/docs/challenges/knapsack.md +++ b/docs/challenges/knapsack.md @@ -1,50 +1,54 @@ # Knapsack Problem -The quadratic knapsack problem is one of the most popular variants of the single knapsack problem with applications in many optimization problems. The aim is to maximise the value of individual items placed in the knapsack while satisfying a weight constraint. However, pairs of items also have interaction values which may be negative or positive that are added to the total value within the knapsack. +The quadratic knapsack problem is one of the most popular variants of the single knapsack problem, with applications in many optimization contexts. The aim is to maximize the value of individual items placed in the knapsack while satisfying a weight constraint. However, pairs of items also have positive interaction values, contributing to the total value within the knapsack. -# Example + +## Challenge Overview For our challenge, we use a version of the quadratic knapsack problem with configurable difficulty, where the following two parameters can be adjusted in order to vary the difficulty of the challenge: - Parameter 1: $num\textunderscore{ }items$ is the number of items from which you need to select a subset to put in the knapsack. -- Parameter 2: $better\textunderscore{ }than\textunderscore{ }baseline \geq 1$ is the factor by which a solution must be better than the baseline value [link TIG challenges for explanation of baseline value]. - +- Parameter 2: $better\textunderscore{ }than\textunderscore{ }baseline \geq 1$ is the factor by which a solution must be better than the baseline value. The larger the $num\textunderscore{ }items$, the more number of possible $S_{knapsack}$, making the challenge more difficult. Also, the higher $better\textunderscore{ }than\textunderscore{ }baseline$, the less likely a given $S_{knapsack}$ will be a solution, making the challenge more difficult. -The weight $w_i$ of each of the $num\textunderscore{ }items$ is an integer, chosen independently, uniformly at random, and such that each of the item weights $1 <= w_i <= 50$, for $i=1,2,...,num\textunderscore{ }items$. The individual values of the items $v_i$ are selected by random from the range $50 <= v_i <= 100$, and the interaction values of pairs of items $V_{ij}$ are selected by random from the range $-50 <= V_{ij} <= 50$. +The weight $w_i$ of each of the $num\textunderscore{ }items$ is an integer, chosen independently, uniformly at random, and such that each of the item weights $1 <= w_i <= 50$, for $i=1,2,...,num\textunderscore{ }items$. The values of the items are nonzero with a density of 25%, meaning they have a 25% probability of being nonzero. The nonzero individual values of the item, $v_i$, and the nonzero interaction values of pairs of items, $V_{ij}$, are selected at random from the range $[1,100]$. -The total value of a knapsack is determined by summing up the individual values of items in the knapsack, as well as the interaction values of every pair of items $(i,j)$ where $i > j$ in the knapsack: +The total value of a knapsack is determined by summing up the individual values of items in the knapsack, as well as the interaction values of every pair of items \((i,j)\), where \( i > j \), in the knapsack: -$$V_{knapsack} = \sum_{i \in knapsack}{v_i} + \sum_{(i,j)\in knapsack}{V_{ij}}$$ +$$ +V_{knapsack} = \sum_{i \in knapsack}{v_i} + \sum_{(i,j)\in knapsack}{V_{ij}} +$$ We impose a weight constraint $W(S_{knapsack}) <= 0.5 \cdot W(S_{all})$, where the knapsack can hold at most half the total weight of all items. -Consider an example of a challenge instance with `num_items=4` and `better_than_baseline = 1.10`. Let the baseline value be 150: +# Example + +Consider an example of a challenge instance with `num_items=4` and `better_than_baseline = 1.50`. Let the baseline value be 46: ``` -weights = [26, 20, 39, 13] -individual_values = [63, 87, 52, 97] -interaction_values = [ 0, 23, -18, -37 - 23, 0, 42, -28 - -18, 42, 0, 32 - -37, -28, 32, 0] -max_weight = 60 -min_value = baseline*better_than_baseline = 165 +weights = [39, 29, 15, 43] +individual_values = [0, 14, 0, 75] +interaction_values = [ 0, 0, 0, 0 + 0, 0, 32, 0 + 0, 32, 0, 0 + 0, 0, 0, 0] +max_weight = 63 +min_value = baseline*better_than_baseline = 69 ``` -The objective is to find a set of items where the total weight is at most 60 but has a total value of at least 165. +The objective is to find a set of items where the total weight is at most 63 but has a total value of at least 69. Now consider the following selection: ``` -selected_items = [0, 1, 3] +selected_items = [2, 3] ``` -When evaluating this selection, we can confirm that the total weight is less than 60, and the total value is more than 165, thereby this selection of items is a solution: +When evaluating this selection, we can confirm that the total weight is less than 63, and the total value is more than 69, thereby this selection of items is a solution: -* Total weight = 26 + 20 + 13 = 59 -* Total value = 63 + 52 + 97 + 23 - 37 - 28 = 170 +* Total weight = 15 + 43 = 58 +* Total value = 0 + 75 + 0 = 75 # Our Challenge -In TIG, the baseline value is determined by a greedy algorithm that simply iterates through items sorted by potential value to weight ratio, adding them if knapsack is still below the weight constraint. +In TIG, the baseline value is determined by a two-stage approach. First, items are selected based on their value-to-weight ratio, including interaction values, until the capacity is reached. Then, a tabu-based local search refines the solution by swapping items to improve value while avoiding reversals, with early termination for unpromising swaps. diff --git a/scripts/test_algorithm.sh b/scripts/test_algorithm.sh index 14b04b2..fb6ebf2 100644 --- a/scripts/test_algorithm.sh +++ b/scripts/test_algorithm.sh @@ -76,6 +76,12 @@ if ! is_positive_integer "$num_workers"; then echo "Error: Number of workers must be a positive integer." exit 1 fi +read -p "Enter max fuel (default is 10000000000): " max_fuel +max_fuel=${max_fuel:-1} +if ! is_positive_integer "$max_fuel"; then + echo "Error: Max fuel must be a positive integer." + exit 1 +fi read -p "Enable debug mode? (leave blank to disable) " enable_debug if [[ -n $enable_debug ]]; then debug_mode=true @@ -117,7 +123,7 @@ while [ $remaining_nonces -gt 0 ]; do start_time=$(date +%s%3N) stdout=$(mktemp) stderr=$(mktemp) - ./target/release/tig-worker compute_batch "$SETTINGS" "random_string" $current_nonce $nonces_to_compute $power_of_2_nonces $REPO_DIR/tig-algorithms/wasm/$CHALLENGE/$ALGORITHM.wasm --workers $nonces_to_compute >"$stdout" 2>"$stderr" + ./target/release/tig-worker compute_batch "$SETTINGS" "random_string" $current_nonce $nonces_to_compute $power_of_2_nonces $REPO_DIR/tig-algorithms/wasm/$CHALLENGE/$ALGORITHM.wasm --workers $nonces_to_compute --fuel $max_fuel >"$stdout" 2>"$stderr" exit_code=$? output_stdout=$(cat "$stdout") output_stderr=$(cat "$stderr") diff --git a/tig-challenges/src/knapsack.rs b/tig-challenges/src/knapsack.rs index c6e8245..b098ce4 100644 --- a/tig-challenges/src/knapsack.rs +++ b/tig-challenges/src/knapsack.rs @@ -78,21 +78,41 @@ impl crate::ChallengeTrait for Challenge { fn generate_instance(seed: [u8; 32], difficulty: &Difficulty) -> Result { let mut rng = SmallRng::from_seed(StdRng::from_seed(seed).gen()); + // Set constant density for value generation + let density = 0.25; + // Generate weights w_i in the range [1, 50] let weights: Vec = (0..difficulty.num_items) .map(|_| rng.gen_range(1..=50)) .collect(); - // Generate values v_i in the range [50, 100] + + // Generate values v_i in the range [1, 100] with density probability, 0 otherwise let values: Vec = (0..difficulty.num_items) - .map(|_| rng.gen_range(50..=100)) + .map(|_| { + if rng.gen_bool(density) { + rng.gen_range(1..=100) + } else { + 0 + } + }) .collect(); - // Generate interactive values V_ij in the range [1, 50], with V_ij == V_ji and V_ij where i==j is 0. + // Generate interaction values V_ij with the following properties: + // - V_ij == V_ji (symmetric matrix) + // - V_ii == 0 (diagonal is zero) + // - Values are in range [1, 100] with density probability, 0 otherwise let mut interaction_values: Vec> = vec![vec![0; difficulty.num_items]; difficulty.num_items]; + for i in 0..difficulty.num_items { for j in (i + 1)..difficulty.num_items { - let value = rng.gen_range(-50..=50); + let value = if rng.gen_bool(density) { + rng.gen_range(1..=100) + } else { + 0 + }; + + // Set both V_ij and V_ji due to symmetry interaction_values[i][j] = value; interaction_values[j][i] = value; } @@ -102,30 +122,145 @@ impl crate::ChallengeTrait for Challenge { // Precompute the ratio between the total value (value + sum of interactive values) and // weight for each item. Pair the ratio with the item's weight and index - let mut value_weight_ratios: Vec<(usize, f32, u32)> = (0..difficulty.num_items) + let mut item_values: Vec<(usize, f32)> = (0..difficulty.num_items) .map(|i| { let total_value = values[i] as i32 + interaction_values[i].iter().sum::(); - let weight = weights[i]; - let ratio = total_value as f32 / weight as f32; - (i, ratio, weight) + let ratio = total_value as f32 / weights[i] as f32; + (i, ratio) }) .collect(); - // Sort the list of tuples by value-to-weight ratio in descending order - value_weight_ratios - .sort_by(|&(_, ratio_a, _), &(_, ratio_b, _)| ratio_b.partial_cmp(&ratio_a).unwrap()); + // Sort the list of ratios in descending order + item_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + // Step 1: Initial solution obtained by greedily selecting items based on value-weight ratio + let mut selected_items = Vec::with_capacity(difficulty.num_items); + let mut unselected_items = Vec::with_capacity(difficulty.num_items); let mut total_weight = 0; - let mut selected_indices = Vec::new(); - for &(i, _, weight) in &value_weight_ratios { - if total_weight + weight <= max_weight { - selected_indices.push(i); - total_weight += weight; + let mut total_value = 0; + let mut is_selected = vec![false; difficulty.num_items]; + + for &(item, _) in &item_values { + if total_weight + weights[item] <= max_weight { + total_weight += weights[item]; + total_value += values[item] as i32; + + for &prev_item in &selected_items { + total_value += interaction_values[item][prev_item]; + } + selected_items.push(item); + is_selected[item] = true; + } else { + unselected_items.push(item); } } - selected_indices.sort(); - let mut min_value = calculate_total_value(&selected_indices, &values, &interaction_values); + // Step 2: Improvement of solution with Local Search and Tabu-List + // Precompute sum of interaction values with each selected item for all items + let mut interaction_sum_list = vec![0; difficulty.num_items]; + for x in 0..difficulty.num_items { + interaction_sum_list[x] = values[x] as i32; + for &item in &selected_items { + interaction_sum_list[x] += interaction_values[x][item]; + } + } + + let mut min_selected_item_values = i32::MAX; + for x in 0..difficulty.num_items { + if is_selected[x] { + min_selected_item_values = min_selected_item_values.min(interaction_sum_list[x]); + } + } + + // Optimized local search with tabu list + let max_iterations = 100; + let mut tabu_list = vec![0; difficulty.num_items]; + + for _ in 0..max_iterations { + let mut best_improvement = 0; + let mut best_swap = None; + + for i in 0..unselected_items.len() { + let new_item = unselected_items[i]; + if tabu_list[new_item] > 0 { + continue; + } + + let new_item_values_sum = interaction_sum_list[new_item]; + if new_item_values_sum < best_improvement + min_selected_item_values { + continue; + } + + // Compute minimal weight of remove_item required to put new_item + let min_weight = + weights[new_item] as i32 - (max_weight as i32 - total_weight as i32); + for j in 0..selected_items.len() { + let remove_item = selected_items[j]; + if tabu_list[remove_item] > 0 { + continue; + } + + // Don't check the weight if there is enough remaining capacity + if min_weight > 0 { + // Skip a remove_item if the remaining capacity after removal is insufficient to push a new_item + let removed_item_weight = weights[remove_item] as i32; + if removed_item_weight < min_weight { + continue; + } + } + + let remove_item_values_sum = interaction_sum_list[remove_item]; + let value_diff = new_item_values_sum + - remove_item_values_sum + - interaction_values[new_item][remove_item]; + + if value_diff > best_improvement { + best_improvement = value_diff; + best_swap = Some((i, j)); + } + } + } + + if let Some((unselected_index, selected_index)) = best_swap { + let new_item = unselected_items[unselected_index]; + let remove_item = selected_items[selected_index]; + + selected_items.swap_remove(selected_index); + unselected_items.swap_remove(unselected_index); + selected_items.push(new_item); + unselected_items.push(remove_item); + + is_selected[new_item] = true; + is_selected[remove_item] = false; + + total_value += best_improvement; + total_weight = total_weight + weights[new_item] - weights[remove_item]; + + // Update sum of interaction values after swapping items + min_selected_item_values = i32::MAX; + for x in 0..difficulty.num_items { + interaction_sum_list[x] += + interaction_values[x][new_item] - interaction_values[x][remove_item]; + if is_selected[x] { + min_selected_item_values = + min_selected_item_values.min(interaction_sum_list[x]); + } + } + + // Update tabu list + tabu_list[new_item] = 3; + tabu_list[remove_item] = 3; + } else { + break; // No improvement found, terminate local search + } + + // Decrease tabu counters + for t in tabu_list.iter_mut() { + *t = if *t > 0 { *t - 1 } else { 0 }; + } + } + + let mut min_value = calculate_total_value(&selected_items, &values, &interaction_values); min_value = (min_value as f32 * (1.0 + difficulty.better_than_baseline as f32 / 1000.0)) .round() as u32; diff --git a/tig-protocol/src/contracts/algorithms.rs b/tig-protocol/src/contracts/algorithms.rs index 3cb824f..4a48514 100644 --- a/tig-protocol/src/contracts/algorithms.rs +++ b/tig-protocol/src/contracts/algorithms.rs @@ -216,10 +216,9 @@ pub(crate) async fn update(cache: &mut AddBlockCache) { if let Some(breakthrough_id) = &active_algorithms_details[algorithm_id].breakthrough_id { - active_breakthroughs_block_data - .get_mut(breakthrough_id) - .unwrap() - .adoption += adoption; + if let Some(block_data) = active_breakthroughs_block_data.get_mut(breakthrough_id) { + block_data.adoption += adoption; + } } } } diff --git a/tig-protocol/src/contracts/players.rs b/tig-protocol/src/contracts/players.rs index 7991a8c..407bd28 100644 --- a/tig-protocol/src/contracts/players.rs +++ b/tig-protocol/src/contracts/players.rs @@ -273,7 +273,7 @@ pub(crate) async fn update(cache: &mut AddBlockCache) { for deposit in active_deposit_details.values() { let total_time = PreciseNumber::from(deposit.end_timestamp - deposit.start_timestamp); for i in 0..lock_period_cap { - if round_timestamps[i + 1] <= deposit.start_timestamp { + if i + 1 < lock_period_cap && round_timestamps[i + 1] <= deposit.start_timestamp { continue; } if round_timestamps[i] >= deposit.end_timestamp { diff --git a/tig-worker/src/main.rs b/tig-worker/src/main.rs index 260ad47..e2bea8b 100644 --- a/tig-worker/src/main.rs +++ b/tig-worker/src/main.rs @@ -28,7 +28,7 @@ fn cli() -> Command { .arg(arg!( "Path to a wasm file").value_parser(clap::value_parser!(PathBuf))) .arg( arg!(--fuel [FUEL] "Optional maximum fuel parameter for WASM VM") - .default_value("2000000000") + .default_value("10000000000") .value_parser(clap::value_parser!(u64)), ) .arg(