diff --git a/tig-algorithms/src/knapsack/fast_and_fun/README.md b/tig-algorithms/src/knapsack/fast_and_fun/README.md new file mode 100644 index 0000000..f955b72 --- /dev/null +++ b/tig-algorithms/src/knapsack/fast_and_fun/README.md @@ -0,0 +1,23 @@ +# TIG Code Submission + +## Submission Details + +* **Challenge Name:** knapsack +* **Submission Name:** fast_and_fun +* **Copyright:** 2025 Thibaut Vidal +* **Identity of Submitter:** Thibaut Vidal +* **Identity of Creator of Algorithmic Method:** null +* **Unique Algorithm Identifier (UAI):** null + +## License + +The files in this folder are under the following licenses: +* TIG Benchmarker Outbound License +* TIG Commercial License +* TIG Inbound Game License +* TIG Innovator Outbound Game License +* TIG Open Data License +* TIG THV Game License + +Copies of the licenses can be obtained at: +https://github.com/tig-foundation/tig-monorepo/tree/main/docs/licenses \ No newline at end of file diff --git a/tig-algorithms/src/knapsack/fast_and_fun/mod.rs b/tig-algorithms/src/knapsack/fast_and_fun/mod.rs new file mode 100644 index 0000000..9dfb550 --- /dev/null +++ b/tig-algorithms/src/knapsack/fast_and_fun/mod.rs @@ -0,0 +1,452 @@ +use anyhow::Result; +use serde_json::{Map, Value}; +use std::cmp::Ordering; +use tig_challenges::knapsack::*; + +#[derive(Clone, Copy)] +struct Params { + diff_lim: usize, + core_half_dp: usize, + core_half_ls: usize, + n_maxils: usize, +} +impl Default for Params { + fn default() -> Self { + Self { + diff_lim: 3, + core_half_dp: 30, + core_half_ls: 50, + n_maxils: 3, + } + } +} + +#[inline] +fn weight_of(ch: &Challenge, items: &[usize]) -> i64 { + items.iter().map(|&i| ch.weights[i] as i64).sum() +} + +fn round0_scores(ch: &Challenge, out: &mut [i32]) { + let n = ch.difficulty.num_items; + for i in 0..n { + let row_sum: i32 = ch.interaction_values[i].iter().sum(); + out[i] = ch.values[i] as i32 + row_sum; + } +} + +struct State<'a> { + ch: &'a Challenge, + selected_bit: Vec, + contrib: Vec, + total_value: i64, + total_weight: i64, +} + +impl<'a> State<'a> { + fn new_empty(ch: &'a Challenge) -> Self { + let n = ch.difficulty.num_items; + let mut contrib = vec![0i32; n]; + for i in 0..n { + contrib[i] = ch.values[i] as i32; + } + Self { + ch, + selected_bit: vec![false; n], + contrib, + total_value: 0, + total_weight: 0, + } + } + fn selected_items(&self) -> Vec { + (0..self.ch.difficulty.num_items) + .filter(|&i| self.selected_bit[i]) + .collect() + } + #[inline] + fn capacity(&self) -> i64 { + self.ch.max_weight as i64 + } + #[inline] + fn slack(&self) -> i64 { + self.capacity() - self.total_weight + } + fn add_item(&mut self, i: usize) { + self.selected_bit[i] = true; + self.total_value += self.contrib[i] as i64; + self.total_weight += self.ch.weights[i] as i64; + let n = self.ch.difficulty.num_items; + for k in 0..n { + self.contrib[k] += self.ch.interaction_values[k][i] as i32; + } + } + fn remove_item(&mut self, j: usize) { + self.total_value -= self.contrib[j] as i64; + self.total_weight -= self.ch.weights[j] as i64; + let n = self.ch.difficulty.num_items; + for k in 0..n { + self.contrib[k] -= self.ch.interaction_values[k][j] as i32; + } + self.selected_bit[j] = false; + } + fn replace_item(&mut self, rm: usize, cand: usize) { + let w_c = self.ch.weights[cand] as i64; + if self.slack() >= w_c { + self.add_item(cand); + self.remove_item(rm); + } else { + self.remove_item(rm); + self.add_item(cand); + } + } + fn restore_snapshot( + &mut self, + snapshot_sel: &[usize], + snapshot_contrib: Vec, + snap_value: i64, + ) { + self.selected_bit.fill(false); + for &i in snapshot_sel { + self.selected_bit[i] = true; + } + self.contrib = snapshot_contrib; + self.total_value = snap_value; + self.total_weight = weight_of(self.ch, snapshot_sel); + } + #[inline] + fn remove_from_vec(v: &mut Vec, x: usize) { + if let Some(pos) = v.iter().position(|&y| y == x) { + v.swap_remove(pos); + } + } +} + +fn build_initial_solution(state: &mut State, order_scores: &[i32]) { + let n = state.ch.difficulty.num_items; + let mut order: Vec = (0..n).collect(); + order.sort_unstable_by(|&a, &b| { + let da = (order_scores[a] as f64) / (state.ch.weights[a] as f64); + let db = (order_scores[b] as f64) / (state.ch.weights[b] as f64); + db.partial_cmp(&da).unwrap_or(Ordering::Equal) + }); + for &i in &order { + let w = state.ch.weights[i] as i64; + if state.total_weight + w <= state.capacity() { + state.add_item(i); + } + } +} + +fn integer_core_target(ch: &Challenge, contrib: &[i32], core_half_dp: usize) -> Vec { + let n = ch.difficulty.num_items; + let mut order: Vec = (0..n).collect(); + order.sort_unstable_by(|&a, &b| { + let da = (contrib[a] as f64) / (ch.weights[a] as f64); + let db = (contrib[b] as f64) / (ch.weights[b] as f64); + db.partial_cmp(&da).unwrap_or(Ordering::Equal) + }); + let mut pref_w: i64 = 0; + let mut break_idx: usize = order.len().saturating_sub(1); + for (pos, &i) in order.iter().enumerate() { + let w = ch.weights[i] as i64; + if pref_w + w > ch.max_weight as i64 { + break_idx = pos; + break; + } + pref_w += w; + } + let left = break_idx.saturating_sub(core_half_dp); + let right = (break_idx + core_half_dp + 1).min(n); + let locked = &order[..left]; + let core = &order[left..right]; + let used_locked: i64 = locked.iter().map(|&i| ch.weights[i] as i64).sum(); + let rem_cap = ((ch.max_weight as i64) - used_locked).max(0) as usize; + let myw = rem_cap; + let myk = core.len(); + let mut dp: Vec = vec![i64::MIN / 4; myw + 1]; + dp[0] = 0; + let mut choose: Vec = vec![0u8; myk * (myw + 1)]; + let mut w_hi: usize = 0; + for (t, &it) in core.iter().enumerate() { + let wt = ch.weights[it] as usize; + if wt > myw { + continue; + } + let val = contrib[it] as i64; + let new_hi = (w_hi + wt).min(myw); + for w in (wt..=new_hi).rev() { + let cand = dp[w - wt] + val; + if cand > dp[w] { + dp[w] = cand; + choose[t * (myw + 1) + w] = 1; + } + } + w_hi = new_hi; + } + let mut selected: Vec = locked.to_vec(); + let mut w_star = (0..=myw).max_by_key(|&w| dp[w]).unwrap_or(0); + for t in (0..myk).rev() { + let it = core[t]; + let wt = ch.weights[it] as usize; + if wt <= w_star && choose[t * (myw + 1) + w_star] == 1 { + selected.push(it); + w_star -= wt; + } + } + selected.sort_unstable(); + selected +} + +fn apply_dp_target_via_ops(state: &mut State, target_sel: &[usize]) { + let n = state.ch.difficulty.num_items; + let mut in_target = vec![false; n]; + for &i in target_sel { + in_target[i] = true; + } + let mut to_remove: Vec = Vec::new(); + for i in 0..n { + if state.selected_bit[i] && !in_target[i] { + to_remove.push(i); + } + } + let mut to_add: Vec = Vec::new(); + for &i in target_sel { + if !state.selected_bit[i] { + to_add.push(i); + } + } + for &r in &to_remove { + state.remove_item(r); + } + for &a in &to_add { + state.add_item(a); + } +} + +fn build_ls_windows(state: &State, core_half_ls: usize) -> (Vec, Vec) { + let n = state.ch.difficulty.num_items; + let mut order: Vec = (0..n).collect(); + order.sort_unstable_by(|&a, &b| { + let da = (state.contrib[a] as f64) / (state.ch.weights[a] as f64); + let db = (state.contrib[b] as f64) / (state.ch.weights[b] as f64); + db.partial_cmp(&da).unwrap_or(Ordering::Equal) + }); + let mut best_unused = Vec::with_capacity(core_half_ls); + for &i in &order { + if !state.selected_bit[i] { + best_unused.push(i); + if best_unused.len() >= core_half_ls { + break; + } + } + } + let mut worst_used = Vec::with_capacity(core_half_ls); + for &i in order.iter().rev() { + if state.selected_bit[i] { + worst_used.push(i); + if worst_used.len() >= core_half_ls { + break; + } + } + } + (best_unused, worst_used) +} + +fn apply_best_add_windowed( + state: &mut State, + best_unused: &mut Vec, + worst_used: &mut Vec, +) -> bool { + let slack = state.slack(); + if slack <= 0 { + return false; + } + let mut best: Option<(usize, i64)> = None; + for &cand in &*best_unused { + let w = state.ch.weights[cand] as i64; + if w > slack { + continue; + } + let delta = state.contrib[cand] as i64; + if delta > 0 && best.map_or(true, |(_, bd)| delta > bd) { + best = Some((cand, delta)); + } + } + if let Some((cand, _)) = best { + state.add_item(cand); + State::remove_from_vec(best_unused, cand); + worst_used.push(cand); + true + } else { + false + } +} + +fn apply_best_swap11_equal_windowed( + state: &mut State, + best_unused: &mut Vec, + worst_used: &mut Vec, +) -> bool { + let mut best: Option<(usize, usize, i64)> = None; + for &rm in &*worst_used { + let w_rm = state.ch.weights[rm]; + for &cand in &*best_unused { + if state.ch.weights[cand] != w_rm { + continue; + } + let delta = (state.contrib[cand] as i64) + - (state.contrib[rm] as i64) + - (state.ch.interaction_values[cand][rm] as i64); + if delta > 0 && best.map_or(true, |(_, _, bd)| delta > bd) { + best = Some((cand, rm, delta)); + } + } + } + if let Some((cand, rm, _)) = best { + state.replace_item(rm, cand); + State::remove_from_vec(worst_used, rm); + best_unused.push(rm); + State::remove_from_vec(best_unused, cand); + worst_used.push(cand); + true + } else { + false + } +} + +fn apply_best_swap_diff_reduce_windowed( + state: &mut State, + params: &Params, + best_unused: &mut Vec, + worst_used: &mut Vec, +) -> bool { + let mut best: Option<(usize, usize, i64)> = None; + for &rm in &*worst_used { + let w_rm = state.ch.weights[rm] as i64; + for &cand in &*best_unused { + let w_c = state.ch.weights[cand] as i64; + if w_c >= w_rm { + continue; + } + let dw = (w_rm - w_c) as usize; + if dw == 0 || dw > params.diff_lim { + continue; + } + let delta = (state.contrib[cand] as i64) + - (state.contrib[rm] as i64) + - (state.ch.interaction_values[cand][rm] as i64); + if delta > 0 && best.map_or(true, |(_, _, bd)| delta > bd) { + best = Some((cand, rm, delta)); + } + } + } + if let Some((cand, rm, _)) = best { + state.replace_item(rm, cand); + State::remove_from_vec(worst_used, rm); + best_unused.push(rm); + State::remove_from_vec(best_unused, cand); + worst_used.push(cand); + true + } else { + false + } +} + +fn apply_best_swap_diff_increase_windowed( + state: &mut State, + params: &Params, + best_unused: &mut Vec, + worst_used: &mut Vec, +) -> bool { + if state.slack() <= 0 { + return false; + } + let mut best: Option<(usize, usize, f64)> = None; + for &rm in &*worst_used { + let w_rm = state.ch.weights[rm] as i64; + for &cand in &*best_unused { + let w_c = state.ch.weights[cand] as i64; + if w_c <= w_rm { + continue; + } + let dw = (w_c - w_rm) as i64; + if dw as usize > params.diff_lim { + continue; + } + if state.slack() < dw { + continue; + } + let delta = (state.contrib[cand] as i64) + - (state.contrib[rm] as i64) + - (state.ch.interaction_values[cand][rm] as i64); + if delta > 0 { + let ratio = (delta as f64) / (dw as f64); + if best.map_or(true, |(_, _, br)| ratio > br) { + best = Some((cand, rm, ratio)); + } + } + } + } + if let Some((cand, rm, _)) = best { + state.replace_item(rm, cand); + State::remove_from_vec(worst_used, rm); + best_unused.push(rm); + State::remove_from_vec(best_unused, cand); + worst_used.push(cand); + true + } else { + false + } +} + +fn local_search_vnd(state: &mut State, params: &Params) { + let (mut best_unused, mut worst_used) = build_ls_windows(state, params.core_half_ls); + loop { + if apply_best_add_windowed(state, &mut best_unused, &mut worst_used) { + continue; + } + if apply_best_swap_diff_reduce_windowed(state, params, &mut best_unused, &mut worst_used) { + continue; + } + if apply_best_swap11_equal_windowed(state, &mut best_unused, &mut worst_used) { + continue; + } + if apply_best_swap_diff_increase_windowed(state, params, &mut best_unused, &mut worst_used) + { + continue; + } + break; + } +} + +pub fn solve_challenge( + challenge: &Challenge, + save_solution: &dyn Fn(&Solution) -> anyhow::Result<()>, + hyperparameters: &Option>, +) -> anyhow::Result<()> { + let params = Params::default(); + let n = challenge.difficulty.num_items; + let mut build_scores = vec![0i32; n]; + round0_scores(challenge, &mut build_scores); + + let mut state = State::new_empty(challenge); + build_initial_solution(&mut state, &build_scores); + local_search_vnd(&mut state, ¶ms); + + for _it in 0..params.n_maxils { + let prev_sel = state.selected_items(); + let prev_val = state.total_value; + let prev_contrib = state.contrib.clone(); + let target = integer_core_target(challenge, &state.contrib, params.core_half_dp); + apply_dp_target_via_ops(&mut state, &target); + local_search_vnd(&mut state, ¶ms); + if state.total_value <= prev_val { + state.restore_snapshot(&prev_sel, prev_contrib, prev_val); + break; + } + } + + let mut items = state.selected_items(); + items.sort_unstable(); + let _ = save_solution(&Solution { items }); + Ok(()) +} diff --git a/tig-algorithms/src/knapsack/mod.rs b/tig-algorithms/src/knapsack/mod.rs index 33ddf59..45dd166 100644 --- a/tig-algorithms/src/knapsack/mod.rs +++ b/tig-algorithms/src/knapsack/mod.rs @@ -190,7 +190,8 @@ // c003_a096 -// c003_a097 +pub mod fast_and_fun; +pub use fast_and_fun as c003_a097; // c003_a098