advent-of-code-2020/day20/day20.cpp

642 lines
18 KiB
C++

#include <folly/String.h>
#include <cmath>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <numeric>
#include <optional>
#include <regex>
#include <string>
#include <vector>
constexpr auto TILE_ID_PATTERN = R"(Tile (\d+):)";
constexpr int NUM_CAMERA_LINES = 10;
// Plus one for the ID line, plus one for the newline after
constexpr int NUM_INPUT_BLOCK_LINES = NUM_CAMERA_LINES + 2;
constexpr char MONSTER_SIGNAL_CHAR = '#';
constexpr auto MONSTER_STR = 1 + R"(
#
# ## ## ###
# # # # # # )";
/**
* Represents a single frame captured by the camera
*/
class CameraFrame {
public:
CameraFrame(int id, const std::vector<std::string> &frame) : id(id), frame(frame) {
}
int getID() const {
return this->id;
}
/**
* Get the contents of this frame
* @return const std::vector<std::string>& The frame contents
*/
const std::vector<std::string> &getFrame() const {
return this->frame;
}
/**
* @return const std::string The top edge of this frame
*/
const std::string getTopEdge() const {
return this->frame.front();
}
/**
* @return const std::string The bottom edge of this frame
*/
const std::string getBottomEdge() const {
return this->frame.back();
}
/**
* @return const std::string The left edge of this frame
*/
const std::string getLeftEdge() const {
std::string edge;
std::transform(
this->frame.cbegin(),
this->frame.cend(),
std::inserter(edge, edge.end()),
[](const std::string &frameLine) { return frameLine.front(); });
return edge;
}
/**
* @return const std::string The right edge of this frame
*/
const std::string getRightEdge() const {
std::string edge;
std::transform(
this->frame.cbegin(),
this->frame.cend(),
std::inserter(edge, edge.end()),
[](const std::string &frameLine) { return frameLine.back(); });
return edge;
}
bool operator==(const CameraFrame &frame) const {
return this->id == frame.id;
}
bool operator!=(const CameraFrame &frame) const {
return !(*this == frame);
}
/**
* Flip this frame along the horizontal axis
*/
void flipFrameVertically() {
std::reverse(this->frame.begin(), this->frame.end());
}
/**
* Flip this frame along the vertical axis
*/
void flipFrameHorizontally() {
std::for_each(this->frame.begin(), this->frame.end(), [](std::string &frameLine) {
std::reverse(frameLine.begin(), frameLine.end());
});
}
/**
* Rotate this frame 90 degrees
*/
void rotateFrame90Deg() {
// This algorithm works by rotating each "ring" of the frame 90 degrees
// Consider the following 5x5 frame
// a b c d e
// f g h i j
// k l m n o
// p q r s t
// u v w x y
// First we rotate the ring that starts with "a b c d e", then "g h i", and once we hit "m", there's nothing to
// rotate, so we're done.
// Init our rotated frame to be the size of our existing (square) frame.
std::vector<std::string> rotatedFrame(this->frame.size(), std::string(this->frame.at(0).size(), ' '));
int ringStartIndex = 0;
int ringEndIndex = this->frame.size() - 1;
while (ringStartIndex <= ringEndIndex) {
// Top Edge -> Right Edge
for (int i = ringStartIndex; i <= ringEndIndex; i++) {
rotatedFrame.at(i).at(ringEndIndex) = frame.at(ringStartIndex).at(i);
}
// Right Edge -> Bottom Edge
for (int i = ringStartIndex; i <= ringEndIndex; i++) {
rotatedFrame.at(ringEndIndex).at(ringEndIndex - i + ringStartIndex) = frame.at(i).at(ringEndIndex);
}
// Bottom Edge -> Left Edge
for (int i = ringStartIndex; i <= ringEndIndex; i++) {
rotatedFrame.at(i).at(ringStartIndex) = frame.at(ringEndIndex).at(i);
}
// Left Edge -> Top Edge
for (int i = ringStartIndex; i <= ringEndIndex; i++) {
rotatedFrame.at(ringStartIndex).at(ringEndIndex - i + ringStartIndex) = frame.at(i).at(ringStartIndex);
}
ringStartIndex++;
ringEndIndex--;
}
this->frame = std::move(rotatedFrame);
}
/**
* Rotate this frame 180 degrees
*/
void rotateFrame180Deg() {
this->rotateFrame90Deg();
this->rotateFrame90Deg();
}
/**
* Rotate this frame 270 degrees
*/
void rotateFrame270Deg() {
this->rotateFrame180Deg();
this->rotateFrame90Deg();
}
/**
* Remove the border of this frame
*/
void removeFrameBorder() {
std::vector<std::string> outFrame;
std::transform(
this->frame.cbegin() + 1,
this->frame.cend() - 1,
std::back_inserter(outFrame),
[](const std::string &frameRow) { return frameRow.substr(1, frameRow.size() - 2); });
this->frame = outFrame;
}
private:
int id;
std::vector<std::string> frame;
};
/**
* Generates all of the possible transforms for a given camera frame.
*/
class TransformGenerator {
public:
class const_iterator {
public:
using difference_type = int;
using value_type = CameraFrame;
using pointer = const value_type *;
using reference = const value_type &;
using iterator_category = std::input_iterator_tag;
const_iterator &operator++() {
++this->baseIterator;
return *this;
}
const_iterator operator++(int) {
const_iterator res = *this;
++(*this);
return res;
}
bool operator==(const const_iterator &other) {
return this->baseIterator == other.baseIterator;
}
bool operator!=(const const_iterator &other) {
return !(*this == other);
}
const value_type operator*() {
auto transform = *(this->baseIterator);
return transform();
}
private:
friend TransformGenerator;
const_iterator(const TransformGenerator &container)
: container(container), baseIterator(container.operationList.cbegin()) {
}
const_iterator(const TransformGenerator &container, const std::vector<std::function<value_type()>>::const_iterator &baseIterator)
: container(container), baseIterator(baseIterator) {
}
const TransformGenerator &container;
std::vector<std::function<value_type()>>::const_iterator baseIterator;
};
TransformGenerator(const CameraFrame &frame)
: frame(frame),
operationList{
[frame]() { return frame; },
[frame = frame]() mutable {
frame.rotateFrame90Deg();
return frame;
},
[frame = frame]() mutable {
frame.rotateFrame180Deg();
return frame;
},
[frame = frame]() mutable {
frame.rotateFrame270Deg();
return frame;
},
[frame = frame]() mutable {
frame.flipFrameVertically();
return frame;
},
[frame = frame]() mutable {
frame.flipFrameHorizontally();
return frame;
},
[frame = frame]() mutable {
frame.flipFrameHorizontally();
frame.rotateFrame90Deg();
return frame;
},
// We don't need one for rotating 180 after flipping, as it's equivalent to flipping vertically
[frame = frame]() mutable {
frame.flipFrameHorizontally();
frame.rotateFrame270Deg();
return frame;
},
} {
}
const_iterator cbegin() const {
return const_iterator(*this);
}
const_iterator cend() const {
return const_iterator(*this, this->operationList.cend());
}
private:
CameraFrame frame;
std::vector<std::function<CameraFrame()>> operationList;
};
std::vector<std::string> readInput(const std::string &filename) {
std::vector<std::string> input;
std::string line;
std::ifstream file(filename);
while (std::getline(file, line)) {
input.push_back(line);
}
return input;
}
/**
* Get the frame ID from an input line containing one
* @param line The frame ID line
* @return int The frame ID
*/
int getFrameIDFromIDLine(const std::string &line) {
std::regex pattern(TILE_ID_PATTERN);
std::smatch matches;
if (!std::regex_match(line, matches, pattern)) {
throw std::invalid_argument("Invalid ID line");
}
return std::stoi(matches[1]);
}
/**
* Parse the puzzle input
* @param input The puzzle input
* @return std::vector<CameraFrame> The frames from the camera input
*/
std::vector<CameraFrame> parseInput(const std::vector<std::string> &input) {
int i = 0;
std::vector<CameraFrame> cameraFrames;
int currentFrameID;
std::vector<std::string> currentCameraFrame;
for (auto it = input.cbegin(); it != input.cend(); (++it, i++)) {
const std::string &line = *it;
int lineIndex = i % NUM_INPUT_BLOCK_LINES;
if (lineIndex == 0) {
currentFrameID = getFrameIDFromIDLine(line);
} else if (line.size() > 0) {
// If the line isn't blank, this is a frame of an image
currentCameraFrame.push_back(line);
} else {
cameraFrames.emplace_back(currentFrameID, currentCameraFrame);
currentCameraFrame.clear();
}
}
// Handle the last frame
cameraFrames.emplace_back(currentFrameID, currentCameraFrame);
return cameraFrames;
}
std::ostream &operator<<(std::ostream &os, const CameraFrame &frame) {
os << "Tile " << frame.getID() << ":" << std::endl;
std::for_each(frame.getFrame().cbegin(), frame.getFrame().cend(), [&os](const std::string &frameLine) {
os << frameLine << std::endl;
});
return os;
}
/**
* Debugging method to print the entire board
* @param board The board
* @param maxRow The maximum row in the board
* @param maxCol The maximum column in the board
*/
void printBoard(const std::map<std::pair<int, int>, CameraFrame> &board, int maxRow, int maxCol, int offset = 0) {
CameraFrame emptyFrame(0, std::vector<std::string>(NUM_CAMERA_LINES, std::string(NUM_CAMERA_LINES, ' ')));
for (int i = 0; i < maxRow; i++) {
std::vector<CameraFrame> rowFrames;
rowFrames.reserve(maxCol);
for (int frameRow = 0; frameRow < NUM_CAMERA_LINES + offset; frameRow++) {
for (int j = 0; j < maxCol; j++) {
auto frame = board.find(std::make_pair(i, j));
auto frameRowStr = (frame == board.end() ? emptyFrame : frame->second).getFrame().at(frameRow);
std::cout << frameRowStr << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
}
}
/**
* Check if the board has all tiles filled
* @param board The board
* @param maxRow The maximum row of the board
* @param maxCol The maximum column of the board
* @return true If the board is filled
* @return false If the board is not filled
*/
bool isBoardFilled(const std::map<std::pair<int, int>, CameraFrame> board, int maxRow, int maxCol) {
for (int i = 0; i < maxRow; i++) {
for (int j = 0; j < maxCol; j++) {
if (board.find(std::make_pair(i, j)) == board.end()) {
return false;
}
}
}
return true;
}
/**
* Find a possible matching frame for this frame
* @param frameToMatch The frame to check
* @param frameMatches A function to check if a given frame matches this one
* @return std::optional<CameraFrame> The matching frame, if ti exists
*/
std::optional<CameraFrame> findPossibleFrame(
const CameraFrame &frameToMatch, std::function<bool(const CameraFrame &)> frameMatches) {
auto transformations = TransformGenerator(frameToMatch);
auto result = std::find_if(transformations.cbegin(), transformations.cend(), [frameMatches](CameraFrame transformedFrame) {
return frameMatches(transformedFrame);
});
if (result == transformations.cend()) {
return std::nullopt;
}
return *result;
}
/**
* Find a board that correctly lines everything up
* @param startingFrame The frame to start with
* @param frames All the other frames available for use
* @param maxRow The maximum row size
* @param maxCol The maximum column size
* @return std::optional<std::map<std::pair<int, int>, CameraFrame>> The board that solves part 1
*/
std::optional<std::map<std::pair<int, int>, CameraFrame>> findLinedUpArrangement(
const CameraFrame &startingFrame, const std::vector<CameraFrame> &frames, int maxRow, int maxCol) {
std::map<std::pair<int, int>, CameraFrame> board;
std::vector<CameraFrame> availableFrames(frames);
board.emplace(std::make_pair(0, 0), startingFrame);
for (int row = 0; row < maxRow; row++) {
for (int col = 0; col < maxCol; col++) {
if (row == 0 && col == 0) {
continue;
}
bool found = false;
for (const CameraFrame &currentFrame : availableFrames) {
std::optional<CameraFrame> matchingFrame;
if (col == 0) {
CameraFrame &aboveFrame = board.at(std::make_pair(row - 1, col));
matchingFrame = findPossibleFrame(currentFrame, [aboveFrame](const CameraFrame &frame) {
return aboveFrame.getBottomEdge() == frame.getTopEdge();
});
} else {
CameraFrame &leftFrame = board.at(std::make_pair(row, col - 1));
matchingFrame = findPossibleFrame(currentFrame, [leftFrame](const CameraFrame &frame) {
return leftFrame.getRightEdge() == frame.getLeftEdge();
});
}
if (matchingFrame) {
found = true;
board.emplace(std::make_pair(row, col), *matchingFrame);
availableFrames.erase(std::remove(availableFrames.begin(), availableFrames.end(), currentFrame));
break;
}
}
if (!found) {
return std::nullopt;
}
}
}
if (!isBoardFilled(board, maxRow, maxCol)) {
return std::nullopt;
}
return board;
}
/**
* Determine the size of the board given the captured frames
* @param frames The frames to use on the board
* @return int The board size
*/
int calculateBoardSize(const std::vector<CameraFrame> &frames) {
return sqrt(frames.size());
}
/**
* Find a board that correctly lines everything up
* @param frames All of the frames
* @return std::optional<std::map<std::pair<int, int>, CameraFrame>> The board that solves part 1
*/
std::map<std::pair<int, int>, CameraFrame> findLinedUpArrangement(const std::vector<CameraFrame> &frames) {
int boardSize = calculateBoardSize(frames);
for (const CameraFrame &frame : frames) {
std::vector<CameraFrame> availableFrames(frames);
availableFrames.erase(std::remove(availableFrames.begin(), availableFrames.end(), frame));
const TransformGenerator transforms(frame);
for (auto it = transforms.cbegin(); it != transforms.cend(); ++it) {
CameraFrame transformedFrame = *it;
auto res = findLinedUpArrangement(transformedFrame, availableFrames, boardSize, boardSize);
if (res) {
return *res;
}
}
}
throw std::invalid_argument("No solution for input");
}
/**
* Get the size of a monster, given a strong representing one.
* @param monster The monster string
* @return std::pair<int, int> The height and width of the monster, as a pair.
*/
std::pair<int, int> getMonsterDimensions(const std::string &monster) {
int height = std::count(monster.cbegin(), monster.cend(), '\n') + 1;
std::vector<std::string> lines;
folly::split("\n", monster, lines);
auto longestIt =
std::max_element(lines.cbegin(), lines.cend(), [](const std::string &line1, const std::string &line2) {
return line1.size() < line2.size();
});
int width = longestIt->size();
return std::make_pair(height, width);
}
/**
* Join a board's frames together into one big board.
* @param board The board to join.
* @param maxRow The highest row in the board
* @param maxCol The highest col in the board
* @return std::vector<std::string> The joined board
*/
std::vector<std::string> joinFullBoard(
const std::map<std::pair<int, int>, CameraFrame> &board, int maxRow, int maxCol) {
std::vector<std::string> outBoard;
// Get an arbitrary frame - doesn't matter what it is, we just need a size.
auto firstFrame = *board.begin();
for (int i = 0; i < maxRow; i++) {
for (int frameRow = 0; frameRow < firstFrame.second.getFrame().size(); frameRow++) {
outBoard.push_back("");
std::string &outRow = outBoard.back();
for (int j = 0; j < maxCol; j++) {
const CameraFrame &frame = board.at(std::make_pair(i, j));
std::string frameRowItems = frame.getFrame().at(frameRow);
outRow += frameRowItems;
}
}
}
return outBoard;
}
/**
* Get the number of characters that contain a monster, within the monster dimensions, starting at (row, col). This only scans for a single monster.
* @param frame The frame to scan
* @param row The row to start at
* @param col The col to start at
* @return int The number of monster characters in this area.
*/
int getNumMonsterChars(const CameraFrame &frame, int row, int col) {
std::pair<int, int> monsterDimensions = getMonsterDimensions(MONSTER_STR);
std::vector<std::string> monsterLines;
folly::split("\n", MONSTER_STR, monsterLines);
int total = 0;
for (int i = 0; i < monsterDimensions.first; i++) {
bool foundMonster = true;
for (int j = 0; j < monsterDimensions.second; j++) {
char monsterChar = monsterLines.at(i).at(j);
char frameChar = frame.getFrame().at(row + i).at(col + j);
if (monsterChar == MONSTER_SIGNAL_CHAR && frameChar != MONSTER_SIGNAL_CHAR) {
foundMonster = false;
break;
}
total += (monsterChar == MONSTER_SIGNAL_CHAR);
}
// If this line didn't find a monster, there can't be any true monster chars
if (!foundMonster) {
return 0;
}
}
return total;
}
long part1(const std::map<std::pair<int, int>, CameraFrame> &board, int boardSize) {
return 1L * board.at(std::make_pair(0, 0)).getID() * board.at(std::make_pair(0, boardSize - 1)).getID() *
board.at(std::make_pair(boardSize - 1, 0)).getID() *
board.at(std::make_pair(boardSize - 1, boardSize - 1)).getID();
}
long part2(const std::map<std::pair<int, int>, CameraFrame> &rawBoard, int boardSize) {
// Remove all of the borders from the board
std::map<std::pair<int, int>, CameraFrame> board(rawBoard);
std::for_each(board.begin(), board.end(), [](std::pair<const std::pair<int, int>, CameraFrame> &frame) {
frame.second.removeFrameBorder();
});
std::pair<int, int> monsterDimensions = getMonsterDimensions(MONSTER_STR);
auto fullBoard = joinFullBoard(board, boardSize, boardSize);
auto fullBoardFrame = CameraFrame(0, fullBoard);
int totalSignalChars =
std::accumulate(fullBoard.cbegin(), fullBoard.cend(), 0, [](int total, const std::string &boardRow) {
return total + std::count(boardRow.cbegin(), boardRow.cend(), MONSTER_SIGNAL_CHAR);
});
auto transforms = TransformGenerator(fullBoardFrame);
for (auto it = transforms.cbegin(); it != transforms.cend(); it++) {
auto transformedFrame = *it;
int totalMonsterChars = 0;
for (int i = 0; i < fullBoard.size() - monsterDimensions.first; i++) {
for (int j = 0; j < fullBoard.at(0).size() - monsterDimensions.second; j++) {
int monsterChars = getNumMonsterChars(transformedFrame, i, j);
totalMonsterChars += monsterChars;
}
}
// Only one orientation will match a monster
if (totalMonsterChars > 0) {
return totalSignalChars - totalMonsterChars;
}
}
throw std::invalid_argument("No valid solution");
}
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << argv[0] << " <input_file>" << std::endl;
return 1;
}
auto input = readInput(argv[1]);
auto parsedInput = parseInput(input);
int boardSize = calculateBoardSize(parsedInput);
auto board = findLinedUpArrangement(parsedInput);
std::cout << part1(board, boardSize) << std::endl;
std::cout << part2(board, boardSize) << std::endl;
}