aoc/year2016/
day04.rs

1//! # Security Through Obscurity
2//!
3//! We can quickly eliminate decoys without needing an expensive sort by checking that the
4//! frequency of checksum letters is non-increasing, letters of equal frequency are in alphabetical
5//! order and that there are no intervening letters in between any two frequencies.
6//!
7//! In part two as the [Caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher) does
8//! not change the length of words, we can also eliminate most candidates with a simple length
9//! check and only decrypt a much smaller number of strings.
10use crate::util::parse::*;
11
12pub struct Room<'a> {
13    name: &'a str,
14    sector_id: u32,
15}
16
17pub fn parse(input: &str) -> Vec<Room<'_>> {
18    let mut valid = Vec::new();
19    let to_index = |b: u8| (b - b'a') as usize;
20
21    'outer: for line in input.lines() {
22        // The sector id and checksum are fixed size leaving whatever is left over as the name.
23        let size = line.len();
24        let name = &line[..size - 11];
25        let sector_id = (&line[size - 10..size - 7]).unsigned();
26        let checksum = &line.as_bytes()[size - 6..size - 1];
27
28        // Count the frequency of each digit, the frequency of each frequency `fof` and the
29        // highest total frequency.
30        let mut freq = [0; 26];
31        let mut fof = [0; 64];
32        let mut highest = 0;
33
34        for b in name.bytes() {
35            if b != b'-' {
36                let index = to_index(b);
37                let current = freq[index];
38                let next = freq[index] + 1;
39
40                freq[index] = next;
41                fof[current] -= 1;
42                fof[next] += 1;
43
44                highest = highest.max(next);
45            }
46        }
47
48        // Initial check.
49        if freq[to_index(checksum[0])] != highest {
50            continue;
51        }
52
53        // Check each pair making sure that the frequency is non-increasing and that there are
54        // no letters in between (`fof` should be zero for all intervening letters).
55        // If the frequency is equal then also make sure letters are in alphabetical order.
56        for w in checksum.windows(2) {
57            let end = freq[to_index(w[0])];
58            let start = freq[to_index(w[1])];
59
60            if start > end || (start == end && w[1] <= w[0]) {
61                continue 'outer;
62            }
63
64            if (start + 1..end).any(|i| fof[i] != 0) {
65                continue 'outer;
66            }
67        }
68
69        valid.push(Room { name, sector_id });
70    }
71
72    valid
73}
74
75pub fn part1(input: &[Room<'_>]) -> u32 {
76    input.iter().map(|room| room.sector_id).sum()
77}
78
79pub fn part2(input: &[Room<'_>]) -> u32 {
80    for &Room { name, sector_id } in input {
81        let bytes = name.as_bytes();
82
83        // Quickly eliminate most rooms as the length of words doesn't change.
84        if bytes.len() == 24 && bytes[9] == b'-' && bytes[16] == b'-' {
85            let mut buffer = String::with_capacity(24);
86
87            // Decrypt potential candidates.
88            for b in name.bytes() {
89                if b == b'-' {
90                    buffer.push(' ');
91                } else {
92                    let rotate = (sector_id % 26) as u8;
93                    let decrypted = (b - b'a' + rotate) % 26 + b'a';
94                    buffer.push(decrypted as char);
95                }
96            }
97
98            if buffer == "northpole object storage" {
99                return sector_id;
100            }
101        }
102    }
103
104    unreachable!()
105}