From 95328895aba4a55f694a18dadf0e82296d86a991 Mon Sep 17 00:00:00 2001 From: Russell Champoux Date: Tue, 6 Jan 2026 13:17:40 -0500 Subject: [PATCH] Solve Day 10 Part 2 with bifurcate algorithm Part 1: Brute force all 2^n button combinations (XOR toggle) Part 2: Bifurcate algorithm - enumerate parity-matching combos, halve remaining targets, and recurse with memoization Algorithm credit: Reddit user tenthmascot https://www.reddit.com/r/adventofcode/comments/1pk87hl/ The key insight is that Part 1 (parity/XOR) helps solve Part 2: 1. Find all button combos that achieve correct target parity 2. Press those once (Phase 1), making all remaining values even 3. Halve and recurse - O(log(max_target)) depth with caching --- README.md | 24 +++---- src/bin/10.rs | 187 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4aee4d0..5ef34fa 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,20 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. | Day | Part 1 | Part 2 | | :---: | :---: | :---: | -| [Day 1](./src/bin/01.rs) | `51.5µs` | `56.4µs` | -| [Day 2](./src/bin/02.rs) | `103.8µs` | `314.5µs` | -| [Day 3](./src/bin/03.rs) | `69.9µs` | `51.1µs` | -| [Day 4](./src/bin/04.rs) | `80.2µs` | `565.0µs` | -| [Day 5](./src/bin/05.rs) | `32.8µs` | `27.1µs` | -| [Day 6](./src/bin/06.rs) | `32.0µs` | `61.2µs` | -| [Day 7](./src/bin/07.rs) | `28.0µs` | `31.6µs` | -| [Day 8](./src/bin/08.rs) | `2.0ms` | `2.8ms` | -| [Day 9](./src/bin/09.rs) | `106.0µs` | `-` | -| [Day 10](./src/bin/10.rs) | `2.5ms` | `-` | -| [Day 11](./src/bin/11.rs) | `76.5µs` | `250.6µs` | +| [Day 1](./src/bin/01.rs) | `96.2µs` | `116.5µs` | +| [Day 2](./src/bin/02.rs) | `122.0µs` | `300.3µs` | +| [Day 3](./src/bin/03.rs) | `115.6µs` | `122.4µs` | +| [Day 4](./src/bin/04.rs) | `232.9µs` | `921.4µs` | +| [Day 5](./src/bin/05.rs) | `54.3µs` | `46.6µs` | +| [Day 6](./src/bin/06.rs) | `121.5µs` | `141.6µs` | +| [Day 7](./src/bin/07.rs) | `76.9µs` | `68.5µs` | +| [Day 8](./src/bin/08.rs) | `9.4ms` | `7.6ms` | +| [Day 9](./src/bin/09.rs) | `125.9µs` | `-` | +| [Day 10](./src/bin/10.rs) | `4.1ms` | `755.7ms` | +| [Day 11](./src/bin/11.rs) | `125.4µs` | `293.5µs` | | [Day 12](./src/bin/12.rs) | `281.2µs` | `-` | -**Total: 9.24ms** +**Total: 780.16ms** --- diff --git a/src/bin/10.rs b/src/bin/10.rs index 01ce1aa..00ab308 100644 --- a/src/bin/10.rs +++ b/src/bin/10.rs @@ -1,6 +1,6 @@ advent_of_code::solution!(10); -/// Parse a machine line into (target_bitmask, buttons_bitmasks) +/// Parse a machine line into (target_bitmask, buttons_bitmasks) for Part 1 fn parse_machine(line: &str) -> Option<(u64, Vec)> { let line = line.trim(); if line.is_empty() { @@ -53,6 +53,52 @@ fn parse_machine(line: &str) -> Option<(u64, Vec)> { Some((target, buttons)) } +/// Parse a machine line for Part 2: (joltage_targets, buttons as index lists) +fn parse_machine_part2(line: &str) -> Option<(Vec, Vec>)> { + let line = line.trim(); + if line.is_empty() { + return None; + } + + // Find where joltages start and end + let joltage_start = line.find('{')?; + let joltage_end = line.find('}')?; + let joltage_str = &line[joltage_start + 1..joltage_end]; + + // Parse joltage targets + let targets: Vec = joltage_str + .split(',') + .filter_map(|s| s.trim().parse().ok()) + .collect(); + + // Find where buttons section ends (at joltage start) + let bracket_end = line.find(']')?; + let button_section = &line[bracket_end + 1..joltage_start]; + + // Parse all buttons + let mut buttons: Vec> = Vec::new(); + let mut i = 0; + let chars: Vec = button_section.chars().collect(); + while i < chars.len() { + if chars[i] == '(' { + let start = i + 1; + while i < chars.len() && chars[i] != ')' { + i += 1; + } + let button_str: String = chars[start..i].iter().collect(); + + let indices: Vec = button_str + .split(',') + .filter_map(|s| s.trim().parse().ok()) + .collect(); + buttons.push(indices); + } + i += 1; + } + + Some((targets, buttons)) +} + /// Find minimum button presses to reach target state (Part 1: toggle/XOR) fn min_presses_part1(target: u64, buttons: &[u64]) -> u64 { let n = buttons.len(); @@ -93,8 +139,143 @@ pub fn part_one(input: &str) -> Option { Some(total) } +/// Find minimum button presses to reach target joltage levels (Part 2: addition) +/// Uses the "bifurcate" algorithm: enumerate parity-matching combos, halve, and recurse +fn min_presses_part2(targets: &[u64], buttons: &[Vec]) -> u64 { + use std::collections::HashMap; + + let n_counters = targets.len(); + + if targets.iter().all(|&t| t == 0) { + return 0; + } + + // Build button masks for XOR parity calculation (like Part 1) + let button_masks: Vec = buttons + .iter() + .map(|button| { + let mut mask = 0u64; + for &idx in button { + if idx < n_counters { + mask |= 1 << idx; + } + } + mask + }) + .collect(); + + // Build button effects lists + let effects: Vec> = buttons + .iter() + .map(|b| b.iter().filter(|&&i| i < n_counters).copied().collect()) + .collect(); + + // Calculate parity mask (which bits are odd) + fn parity_mask(targets: &[u64]) -> u64 { + let mut mask = 0u64; + for (i, &t) in targets.iter().enumerate() { + if t % 2 == 1 { + mask |= 1 << i; + } + } + mask + } + + fn solve( + targets: Vec, + button_masks: &[u64], + effects: &[Vec], + cache: &mut HashMap, u64>, + ) -> u64 { + // Base case: all zeros + if targets.iter().all(|&t| t == 0) { + return 0; + } + + // Check cache + if let Some(&result) = cache.get(&targets) { + return result; + } + + let n_buttons = button_masks.len(); + let target_parity = parity_mask(&targets); + + let mut min_presses = u64::MAX; + + // Enumerate all 2^B button combinations for Phase 1 + for combo in 0..(1u64 << n_buttons) { + // Calculate parity this combo achieves (XOR of selected button masks) + let mut achieved_parity = 0u64; + let mut phase1_count = 0u64; + + for (i, &mask) in button_masks.iter().enumerate() { + if combo & (1 << i) != 0 { + achieved_parity ^= mask; + phase1_count += 1; + } + } + + // Must match target parity for remaining to be all even + if achieved_parity != target_parity { + continue; + } + + // Calculate remaining targets after pressing Phase 1 buttons once each + let mut remaining = targets.clone(); + let mut valid = true; + + for (btn_idx, btn_effects) in effects.iter().enumerate() { + if combo & (1 << btn_idx) != 0 { + for &counter_idx in btn_effects { + if remaining[counter_idx] == 0 { + valid = false; + break; + } + remaining[counter_idx] -= 1; + } + if !valid { + break; + } + } + } + + if !valid { + continue; + } + + // All remaining should now be even - halve them + let halved: Vec = remaining.iter().map(|&r| r / 2).collect(); + + // Recurse on halved problem + let sub_result = solve(halved, button_masks, effects, cache); + if sub_result != u64::MAX { + // Total = Phase 1 presses + 2 * (recursive result) + let total = phase1_count + 2 * sub_result; + min_presses = min_presses.min(total); + } + } + + cache.insert(targets, min_presses); + min_presses + } + + let mut cache = HashMap::new(); + solve(targets.to_vec(), &button_masks, &effects, &mut cache) +} + pub fn part_two(input: &str) -> Option { - None + let mut total = 0; + + for line in input.lines() { + if let Some((targets, buttons)) = parse_machine_part2(line) { + let presses = min_presses_part2(&targets, &buttons); + if presses != u64::MAX { + total += presses; + } + } + } + + Some(total) } #[cfg(test)] @@ -110,6 +291,6 @@ mod tests { #[test] fn test_part_two() { let result = part_two(&advent_of_code::template::read_file("examples", DAY)); - assert_eq!(result, None); + assert_eq!(result, Some(33)); // 10 + 12 + 11 = 33 } }