1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
/* Copyright 2023 Mario Finelli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! Advent of Code 2015 Day 19: <https://adventofcode.com/2015/day/19>
//!
//! Today's challenge was quite hard. It took me a while to even figure out
//! exactly what I was supposed to be doing in the problem. Eventually, after
//! reading the prompt several more times and looking at some solutions on the
//! subreddit I was able to figure out enough to be able to implement my
//! solution.
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::collections::HashSet;
/// The solution for part one of the day nineteen challenge.
///
/// As usual we take the problem input as a string and then start parsing it.
/// The input contains all of the replacements followed by an empty line and
/// then the molecule. So we parse the replacements until we get to the empty
/// line and then the molecule is the final line. Once we have our replacements
/// we loop through them and then we loop and for each of them we loop through
/// the windows of the molecule of the size of the replacement so that we can
/// see if we can do a replacement. If we can then we build a new molecule
/// using the replacement and then add it to our tracking
/// [`std::collections::HashSet`] (we're only supposed to find distinct
/// molecules). Once we've checked all of the possibilities for all of the
/// replacements we just return the length of our set.
///
/// # Example
/// ```rust
/// # use aoc::y15d19::y15d19p1;
/// // probably read this from the input file...
/// let input = "Al => ThF\nAl => ThRnFAr\nB => BCa\nB => TiB\n\nnPBP";
/// assert_eq!(y15d19p1(input), 2);
/// ```
pub fn y15d19p1(input: &str) -> u32 {
let lines: Vec<_> = input.lines().collect();
let num_replacements = lines.len() - 2;
let molecule = lines[lines.len() - 1];
let mut set = HashSet::new();
let mut replacements = Vec::new();
for (i, line) in lines.into_iter().enumerate() {
if i >= num_replacements {
break;
}
let text: Vec<_> = line.split_whitespace().collect();
replacements.push((text[0], text[2]));
}
for (find, replace) in replacements {
for (i, check) in molecule
.chars()
.collect::<Vec<char>>()
.windows(find.len())
.enumerate()
{
let check: String = check.iter().collect();
if check == find {
let new = format!(
"{}{}{}",
substring(molecule, 0, i),
replace,
substring(molecule, i + find.len(), molecule.len())
);
set.insert(new);
}
}
}
set.len().try_into().unwrap()
}
/// The solution for part two of the day nineteen challenge.
///
/// This was very challenging and I had to rely on inspiration from the
/// subreddit to figure out exactly how to solve this problem. As in part one
/// we start by parsing the problem input string to get our replacements and
/// molecule. Then we start a loop that we run until we reach `e`. The strategy
/// here is to work backwards from the end molecule to the start instead of
/// vice-versa as we'd need to implement a breadth-first-search or similar
/// which would quickly explode in how many checks that we need to do and how
/// long that it would take. Then we loop through all of the replacements
/// keeping track of how many times we're able to make a substitution. When we
/// can't make any more substitutions for that replacement (new == original)
/// then we move on to the next one. If, after processing all of the
/// replacements, the length of our "new" molecule after the replacements is
/// the same as the length of our "old" molecule before the replacements (i.e.,
/// we weren't able to do any replacements) then we reset our molecule
/// replacement string back to the original, reset our substitution counter,
/// shuffle the replacements array, and try again. This is the insight that I
/// was able to get from the subreddit, it seems that many people took a
/// similar approach, but as I understand it, it is technically possible to get
/// a non-optimal solution however, it seems that based on many peoples' inputs
/// there is actually only one path from `e` to the molecule so it should be
/// okay. Once we've reached `e` the loop ends and we can return the number of
/// substitutions that we made.
///
/// # Example
/// ```rust
/// # use aoc::y15d19::y15d19p2;
/// // probably read this from the input file...
/// let input = "e => A\ne => B\nA => BB\nB => AA\nB => AB\n\nAB";
/// assert_eq!(y15d19p2(input), 2);
/// ```
pub fn y15d19p2(input: &str) -> u32 {
let mut rng = thread_rng();
let lines: Vec<_> = input.lines().collect();
let num_replacements = lines.len() - 2;
let molecule = lines[lines.len() - 1].to_string();
let mut reduced = molecule.clone();
let mut count = 0;
let mut replacements = Vec::new();
for (i, line) in lines.into_iter().enumerate() {
if i >= num_replacements {
break;
}
let text: Vec<_> = line.split_whitespace().collect();
replacements.push((text[0], text[2]));
}
while reduced != "e" {
let start_len = reduced.len();
for (find, replace) in &replacements {
loop {
let new = reduced.replacen(replace, find, 1);
if new == reduced {
break;
}
count += 1;
reduced = new;
}
}
if start_len == reduced.len() {
replacements.shuffle(&mut rng);
reduced = molecule.clone();
count = 0;
}
}
count
}
/// This function simply returns a substring of the given size from the given
/// starting point as exists in the standard library of many other languages.
fn substring(string: &str, start: usize, end: usize) -> String {
let s: String = string.chars().skip(start).take(end).collect();
s
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_substring() {
let input = "ABCDE";
assert_eq!(substring(input, 1, 2), "BC");
assert_eq!(substring(input, 2, 2), "CD");
}
#[test]
fn it_works() {
let mut input = "H => HO\nH => OH\nO => HH\n\nHOHOHO\n";
assert_eq!(y15d19p1(input), 7);
input = "e => H\ne => O\nH => HO\nH => OH\nO => HH\n\nHOHOHO\n";
assert_eq!(y15d19p2(input), 6);
}
#[test]
fn the_solution() {
let contents = fs::read_to_string("input/2015/day19.txt").unwrap();
assert_eq!(y15d19p1(&contents), 509);
assert_eq!(y15d19p2(&contents), 195);
}
}