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
//! # Security Through Obscurity
//!
//! We can quickly eliminate decoys without needing an expensive sort by checking that the
//! frequency of checksum letters is non-increasing, letters of equal frequency are in alphabetical
//! order and that there are no intervening letters in between any two frequencies.
//!
//! In part two as the [Caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher) does
//! not change the length of words, we can also eliminate most candidates with a simple length
//! check and only decrypt a much smaller number of strings.
use crate::util::parse::*;
pub struct Room<'a> {
name: &'a str,
sector_id: u32,
}
pub fn parse(input: &str) -> Vec<Room<'_>> {
let mut valid = Vec::new();
let to_index = |b: u8| (b - b'a') as usize;
'outer: for line in input.lines() {
// The sector id and checksum are fixed size leaving whatever is left over as the name.
let size = line.len();
let name = &line[..size - 11];
let sector_id = (&line[size - 10..size - 7]).unsigned();
let checksum = line[size - 6..size - 1].as_bytes();
// Count the frequency of each digit, the frequency of each frequency `fof` and the
// highest total frequency.
let mut freq = [0; 26];
let mut fof = [0; 64];
let mut highest = 0;
for b in name.bytes() {
if b != b'-' {
let index = to_index(b);
let current = freq[index];
let next = freq[index] + 1;
freq[index] = next;
fof[current] -= 1;
fof[next] += 1;
highest = highest.max(next);
}
}
// Initial check.
if freq[to_index(checksum[0])] != highest {
continue;
}
// Check each pair making sure that the frequency is non-increasing and that there are
// no letters in between (`fof` should be zero for all intervening letters).
// If the frequency is equal then also make sure letters are in alphabetical order.
for w in checksum.windows(2) {
let end = freq[to_index(w[0])];
let start = freq[to_index(w[1])];
if start > end || (start == end && w[1] <= w[0]) {
continue 'outer;
}
if (start + 1..end).any(|i| fof[i] != 0) {
continue 'outer;
}
}
valid.push(Room { name, sector_id });
}
valid
}
pub fn part1(input: &[Room<'_>]) -> u32 {
input.iter().map(|room| room.sector_id).sum()
}
pub fn part2(input: &[Room<'_>]) -> u32 {
for &Room { name, sector_id } in input {
let bytes = name.as_bytes();
// Quickly eliminate most rooms as the length of words doesn't change.
if bytes.len() == 24 && bytes[9] == b'-' && bytes[16] == b'-' {
let mut buffer = String::with_capacity(24);
// Decrypt potential candidates.
for b in name.bytes() {
if b == b'-' {
buffer.push(' ');
} else {
let rotate = (sector_id % 26) as u8;
let decrypted = (b - b'a' + rotate) % 26 + b'a';
buffer.push(decrypted as char);
}
}
if buffer == "northpole object storage" {
return sector_id;
}
}
}
unreachable!()
}