mirror of
https://github.com/RussellChamp/advent-of-code-2025-cursor.git
synced 2026-10-27 15:45:03 +00:00
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
This commit is contained in:
1 parent
c9fdf547bb
commit
95328895ab
2 files changed
+196
-15
No files matched your search
@@ -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**
|
||||
<!--- benchmarking table --->
|
||||
|
||||
---
|
||||
|
||||
+184
-3
@@ -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<u64>)> {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
@@ -53,6 +53,52 @@ fn parse_machine(line: &str) -> Option<(u64, Vec<u64>)> {
|
||||
Some((target, buttons))
|
||||
}
|
||||
|
||||
/// Parse a machine line for Part 2: (joltage_targets, buttons as index lists)
|
||||
fn parse_machine_part2(line: &str) -> Option<(Vec<u64>, Vec<Vec<usize>>)> {
|
||||
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<u64> = 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<usize>> = Vec::new();
|
||||
let mut i = 0;
|
||||
let chars: Vec<char> = 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<usize> = 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<u64> {
|
||||
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<usize>]) -> 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<u64> = 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<Vec<usize>> = 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<u64>,
|
||||
button_masks: &[u64],
|
||||
effects: &[Vec<usize>],
|
||||
cache: &mut HashMap<Vec<u64>, 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<u64> = 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<u64> {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user