Task 24

Thorn Thaler - <

2025-07-25

1 Setup

1.1 Libraries

library(httr)
library(xml2)
library(magrittr)
library(dplyr)
library(purrr)
library(stringr)
library(tidyr)

1.2 Retrieve Data from AoC

session_cookie <- set_cookies(session = keyring::key_get("AoC-GitHub-Cookie"))
base_url <- paste0("https://adventofcode.com/", params$year, "/day/", params$task_nr)
puzzle <- GET(base_url,
              session_cookie) %>% 
  content(encoding = "UTF-8") %>% 
  xml_find_all("///article") %>% 
  lapply(as.character)

parse_puzzle_data <- function(text_block = readClipboard()) {
  if (length(text_block) == 1L) {
    text_block <- text_block %>% 
      str_split("\n") %>% 
      extract2(1L) %>% 
      keep(nzchar)
  }
  text_block %>% 
    as.integer()
}

puzzle_data <- local({
  GET(paste0(base_url, "/input"),
      session_cookie) %>% 
    content(encoding = "UTF-8") %>% 
    parse_puzzle_data()
})

2 Puzzle Day 24

2.1 Part 1

2.1.1 Description

— Day 24: It Hangs in the Balance —

It’s Christmas Eve, and Santa is loading up the sleigh for this year’s deliveries. However, there’s one small problem: he can’t get the sleigh to balance. If it isn’t balanced, he can’t defy physics, and nobody gets presents this year.

No pressure.

Santa has provided you a list of the weights of every package he needs to fit on the sleigh. The packages need to be split into three groups of exactly the same weight, and every package has to fit. The first group goes in the passenger compartment of the sleigh, and the second and third go in containers on either side. Only when all three groups weigh exactly the same amount will the sleigh be able to fly. Defying physics has rules, you know!

Of course, that’s not the only problem. The first group - the one going in the passenger compartment - needs as few packages as possible so that Santa has some legroom left over. It doesn’t matter how many packages are in either of the other two groups, so long as all of the groups weigh the same.

Furthermore, Santa tells you, if there are multiple ways to arrange the packages such that the fewest possible are in the first group, you need to choose the way where the first group has the smallest quantum entanglement to reduce the chance of any “complications”. The quantum entanglement of a group of packages is the product of their weights, that is, the value you get when you multiply their weights together. Only consider quantum entanglement if the first group has the fewest possible number of packages in it and all groups weigh the same amount.

For example, suppose you have ten packages with weights 1 through 5 and 7 through 11. For this situation, some of the unique first groups, their quantum entanglements, and a way to divide the remaining packages are as follows:

Group 1;             Group 2; Group 3
11 9       (QE= 99); 10 8 2;  7 5 4 3 1
10 9 1     (QE= 90); 11 7 2;  8 5 4 3
10 8 2     (QE=160); 11 9;    7 5 4 3 1
10 7 3     (QE=210); 11 9;    8 5 4 2 1
10 5 4 1   (QE=200); 11 9;    8 7 3 2
10 5 3 2   (QE=300); 11 9;    8 7 4 1
10 4 3 2 1 (QE=240); 11 9;    8 7 5
9 8 3      (QE=216); 11 7 2;  10 5 4 1
9 7 4      (QE=252); 11 8 1;  10 5 3 2
9 5 4 2    (QE=360); 11 8 1;  10 7 3
8 7 5      (QE=280); 11 9;    10 4 3 2 1
8 5 4 3    (QE=480); 11 9;    10 7 2 1
7 5 4 3 1  (QE=420); 11 9;    10 8 2

Of these, although 10 9 1 has the smallest quantum entanglement (90), the configuration with only two packages, 11 9, in the passenger compartment gives Santa the most legroom and wins. In this situation, the quantum entanglement for the ideal configuration is therefore 99. Had there been two configurations with only two packages in the first group, the one with the smaller quantum entanglement would be chosen.

What is the quantum entanglement of the first group of packages in the ideal configuration?

2.1.2 Solution

We use a backtracking algorithm in C++ as always when we need to backtack a significant amount of time with heavy data structures (R would make endless copies and slowing down the process significantly).

The workhorse function is can_partition which checks whether the numbers can be split into partition of the same sum. If we want to partition a vector into k groups, the sum of all numbers must be dividable by k, and the sum divided by k must be the target sum. Then in the backtracking algorithm, we try to add a number to the partition and recurse. If the sum of selected numbers equals the target, we recurse again, decreasing the number of groups by one. Thta is, we found one partition and check whtehr the remaining numbers can also form partitions of the same sum, but we need one group less (the very one we just formed).

If there is only one number left, the algorithm returns true, as per definition this number must equal the target. If at any stage the recursion returned false, we backtrack by deslecting this number and tring the next one. If we did not return with true from the recursion we return false as we could not find a valid partition.

With this workhorse function what is left is to loop through all group sizes from 1 to the number of elements in our vector. We create all combinations of this size, form the first group with these combinations (only if the partial sum equals the target) and check whether we can partition the remaining numbers into k - 1 groups. If this is possible, we store the result, keeping the best solution (minimum group size, and minimum quantum energy).

As soon as we found the first minimum solution, we stop, as we do not need to look for larger groups.

#ifndef STANDALONE
#include <Rcpp.h>
using namespace Rcpp;
#else
#include <iostream>
#endif

#include <algorithm>
#include <functional>
#include <numeric>
#include <vector>

long long product(const std::vector<int>& nums) {
  long long p = 1;
  for (int n : nums)
    p *= n;
  return p;
}

bool can_partition(const std::vector<int>& nums,
                   int k,
                   int target,
                   int start,
                   std::vector<bool>& used) {
  if (k == 1) {
    return true;
  }
  std::function<bool(int, int)> dfs = [&](int idx, int group_sum) {
    if (group_sum == target) {
      return can_partition(nums, k - 1, target, 0, used);
    }
    for (size_t i = idx; i < nums.size(); ++i) {
      if (used[i] || group_sum + nums[i] > target) {
        continue;
      }
      used[i] = true;
      if (dfs(i + 1, group_sum + nums[i])) {
        return true;
      }
      used[i] = false;
    }
    return false;
  };

  return dfs(start, 0);
}

long long find_min_quantum_entanglement(std::vector<int> nums, int k) {
  int total = std::accumulate(nums.begin(), nums.end(), 0);
  if (total % k != 0) {
    return -1;
  }
  int target = total / k;
  std::sort(nums.begin(), nums.end(), std::greater<>());

  int n = nums.size();
  long long best_qe = LLONG_MAX;
  size_t min_group_size = INT_MAX;

  for (int size = 1; size <= n; ++size) {
    std::vector<bool> bitmask(size, true);
    bitmask.resize(n, false);

    do {
      std::vector<int> group;
      for (int i = 0; i < n; ++i) {
        if (bitmask[i]) {
          group.push_back(nums[i]);
        }
      }

      if (std::accumulate(group.begin(), group.end(), 0) != target) {
        continue;
      }

      std::vector<bool> used(n, false);
      for (int i = 0; i < n; ++i) {
        if (bitmask[i]) {
          used[i] = true;
        }
      }

      if (can_partition(nums, k - 1, target, 0, used)) {
        long long qe = product(group);
        if (group.size() < min_group_size || (group.size() == min_group_size && qe < best_qe)) {
          min_group_size = group.size();
          best_qe = qe;
        }
      }

    } while (std::prev_permutation(bitmask.begin(), bitmask.end()));

    if (best_qe != LLONG_MAX) {
      // we found a solution, no need to check groups with more elements
      break;
    }
  }

  return (best_qe == LLONG_MAX) ? -1 : best_qe;
}
#ifndef STANDALONE
// [[Rcpp::export]]
long long find_min_quantum_entanglement(IntegerVector weights, int k) {
  std::vector<int> nums = as<std::vector<int>>(weights);
  return find_min_quantum_entanglement(nums, k);
}
#else
int main() {
  std::vector<int> weights = {1, 2, 3, 4, 5, 7, 8, 9, 10, 11};
  int k = 4;
  long long result = find_min_quantum_entanglement(weights, k);
  std::cout << "Minimum quantum entanglement: " << result << std::endl;
}
#endif
find_min_quantum_entanglement(puzzle_data, 3L)
## [1] 10439961859

2.2 Part 2

2.2.1 Description

— Part Two —

That’s weird… the sleigh still isn’t balancing.

“Ho ho ho”, Santa muses to himself. “I forgot the trunk”.

Balance the sleigh again, but this time, separate the packages into four groups instead of three. The other constraints still apply.

Given the example packages above, this would be some of the new unique first groups, their quantum entanglements, and one way to divide the remaining packages:


11 4    (QE=44); 10 5;   9 3 2 1; 8 7
10 5    (QE=50); 11 4;   9 3 2 1; 8 7
9 5 1   (QE=45); 11 4;   10 3 2;  8 7
9 4 2   (QE=72); 11 3 1; 10 5;    8 7
9 3 2 1 (QE=54); 11 4;   10 5;    8 7
8 7     (QE=56); 11 4;   10 5;    9 3 2 1

Of these, there are three arrangements that put the minimum (two) number of packages in the first group: 11 4, 10 5, and 8 7. Of these, 11 4 has the lowest quantum entanglement, and so it is selected.

Now, what is the quantum entanglement of the first group of packages in the ideal configuration?

2.2.2 Solution

find_min_quantum_entanglement(puzzle_data, 4L)
## [1] 72050269