diff --git a/aoc2020.test/DayTests.cs b/aoc2020.test/DayTests.cs index 753ca82..8529a61 100644 --- a/aoc2020.test/DayTests.cs +++ b/aoc2020.test/DayTests.cs @@ -26,7 +26,7 @@ public class DayTests // [DataRow(typeof(Day17), "293", "1816")] // this one takes too long and i don't want to bother optimizing it [DataRow(typeof(Day18), "12918250417632", "171259538712010")] [DataRow(typeof(Day19), "160", "357")] - //[DataRow(typeof(Day20), "21599955909991", "")] + [DataRow(typeof(Day20), "21599955909991", "2495")] [DataRow(typeof(Day21), "2436", "dhfng,pgblcd,xhkdc,ghlzj,dstct,nqbnmzx,ntggc,znrzgs")] [DataRow(typeof(Day22), "32856", "33805")] [DataRow(typeof(Day23), "36542897", "562136730660")] diff --git a/aoc2020/Day20.cs b/aoc2020/Day20.cs index 8ecd5d0..74c51f4 100644 --- a/aoc2020/Day20.cs +++ b/aoc2020/Day20.cs @@ -5,158 +5,254 @@ namespace aoc2020; /// public sealed class Day20 : Day { - private readonly List _allPermutations; - private readonly List _topLefts; - public Day20() : base(20, "Jurassic Jigsaw") { + } + + public override string Part1() + { + var puzzlePieces = ParsePiecesFromInput(Input.ToArray()); + var connections = FindConnections(puzzlePieces); + var cornerIds = connections + .Select(pair => (piece: pair.Key, nrConnections: pair.Value.Count)) + .Where(connection => connection.nrConnections == 2) + .Select(connection => connection.piece.Id); + + return $"{cornerIds.Aggregate((double)1, (curr, next) => curr * next)}"; + } + + public override string Part2() + { + var puzzlePieces = ParsePiecesFromInput(Input.ToArray()); + var connections = FindConnections(puzzlePieces); + var puzzle = ComposePuzzle(connections); + var lines = ExtractImagesFromPuzzle(puzzle); + var numberSeaMonsters = TagSeaMonsters(lines); + var numberWaves = lines.Select(l => l.Count(c => c == '#')).Sum(); + return $"{numberWaves - numberSeaMonsters * 15}"; + } + + private static IEnumerable ParsePiecesFromInput(string[] input) + { + var pieces = new List(); + const int pieceHeight = 12; + for (var i = pieceHeight; i <= input.Length + 1; i += pieceHeight) + { + var lBound = i - pieceHeight; + var uBound = i - 1; + pieces.Add(PuzzlePiece.CreatePieceFromString(input[lBound..uBound])); + } + + return pieces; + } + + private static Dictionary> FindConnections(IEnumerable puzzlePieces) + { + var sides = new Dictionary(); + var connections = new Dictionary>(); + + void AddConnection(PuzzlePiece p1, PuzzlePiece p2) + { + if (!connections.ContainsKey(p1)) connections.Add(p1, new List()); + if (!connections.ContainsKey(p2)) connections.Add(p2, new List()); + connections[p1].Add(p2); + connections[p2].Add(p1); + } + + foreach (var piece in puzzlePieces) + foreach (var (original, flipped) in piece.SidesWithFlippedPaired.Value) + { + if (sides.ContainsKey(original)) + { + var otherPiece = sides[original]; + AddConnection(piece, otherPiece); + } + else if (sides.ContainsKey(flipped)) + { + var otherPiece = sides[flipped]; + AddConnection(piece, otherPiece); + } + else + { + sides.Add(original, piece); + sides.Add(flipped, piece); + } + } + + return connections; + } + + private static IEnumerable ComposePuzzle(Dictionary> connections) + { + var sideSize = (int)Math.Sqrt(connections.Count); + var unprocessed = connections.Keys.ToHashSet(); + + // step 0: initialize puzzle array + var puzzle = new PuzzlePiece[sideSize][]; + for (var i = 0; i < puzzle.Length; i++) puzzle[i] = new PuzzlePiece[sideSize]; + + // step1: take one of the angles (this will be our 0,0) and find its true orientation/side + var angle = connections.First(x => x.Value.Count == 2).Key; + puzzle[0][0] = RotatePieceToMatch00Position(angle, connections); + unprocessed.Remove(angle); + + // step2: fill the first column + for (var i = 1; i < puzzle.Length; i++) + { + var previousPiece = puzzle[i - 1][0]; + var bottomPiece = connections[previousPiece] + .Where(p => unprocessed.Contains(p)) + .First(p => p.AllSidesWithFlipped.Value.Contains(previousPiece.BottomSide.Value)); + puzzle[i][0] = bottomPiece.TransformSoTopMatchesWith(previousPiece.BottomSide.Value); + unprocessed.Remove(bottomPiece); + } + + // step3: fill each row using the first value as starting point + foreach (var t in puzzle) + for (var c = 1; c < t.Length; c++) + { + var previousPiece = t[c - 1]; + var rightPiece = connections[previousPiece] + .Where(p => unprocessed.Contains(p)) + .First(p => p.AllSidesWithFlipped.Value.Contains(previousPiece.RightSide.Value)); + t[c] = rightPiece.TransformSoLeftMatchesWith(previousPiece.RightSide.Value); + unprocessed.Remove(rightPiece); + } + + return puzzle; + } + + private static char[][] ExtractImagesFromPuzzle(IEnumerable puzzle) + { + const int pieceHeight = 10; var lines = new List(); - var tiles = new List(); - var currentTileId = 0; - foreach (var line in Input) - if (line.StartsWith("Tile ")) - { - currentTileId = int.Parse(line.Split(' ', ':')[1]); - } - else if (line == "") - { - tiles.Add(new(currentTileId, lines.Select(l => l.ToCharArray()).ToArray())); - lines.Clear(); - } - else - { - lines.Add(line); - } + foreach (var t in puzzle) + for (var line = 1; line < pieceHeight - 1; line++) + lines.Add(t.Aggregate("", (current, t1) => current + t1.GetLine(line)[1..^1])); - if (lines.Any()) tiles.Add(new(currentTileId, lines.Select(l => l.ToCharArray()).ToArray())); - - _allPermutations = tiles.SelectMany(t => t.AllPermutations()).ToList(); - _topLefts = _allPermutations - .Where(t => !_allPermutations.Any(t2 => t.TileId != t2.TileId && t.LeftId == t2.RightId) && - !_allPermutations.Any(t2 => t.TileId != t2.TileId && t.TopId == t2.BottomId)) - .ToList(); + return lines.Select(line => line.ToCharArray()).ToArray(); } - private int Roughness(Tile arg) + private static PuzzlePiece RotatePieceToMatch00Position( + PuzzlePiece angle, + IReadOnlyDictionary> connections) { - var seaMonster = new[] - { - " # ", - "# ## ## ###", - " # # # # # # " - }.Select(s => s.ToArray()).ToArray(); + var conn1 = connections[angle][0]; + var conn2 = connections[angle][1]; - const int seaMonsterWidth = 20; - const int seaMonsterHeight = 3; - const int seaMonsterTiles = 15; + var angleSides = angle.Sides.Value.ToHashSet(); + var connectionsSides = conn1.SidesWithFlippedPaired.Value + .Concat(conn2.SidesWithFlippedPaired.Value) + .SelectMany(t => new[] { t.Item1, t.Item2 }); - var placedTiles = new Dictionary<(int x, int y), Tile>(); - bool NotPlaced(Tile tile) => placedTiles!.Values.All(t => t.TileId != tile.TileId); - char Grid(int i, int j) => placedTiles![(i / 8, j / 8)].Pixels[j % 8 + 1][i % 8 + 1]; - - bool HasSeaMonster((int x, int y) location) - { - for (var j = 0; j < seaMonsterHeight; j++) - for (var i = 0; i < seaMonsterWidth; i++) - { - if (seaMonster![j][i] == ' ') continue; - if (Grid(location.x + i, location.y + j) != '#') return false; - } - - return true; - } - - placedTiles[(0, 0)] = arg; - int x = 1, y = 0; - while (true) - { - placedTiles.TryGetValue((x - 1, y), out var left); - placedTiles.TryGetValue((x, y - 1), out var top); - if (left == null && top == null) break; - - var firstMatch = _allPermutations - .Where(t => (left is null || t.LeftId == left.RightId) && - (top is null || t.TopId == top.BottomId)) - .Where(NotPlaced) - .FirstOrDefault(); - - if (firstMatch is not null) - { - placedTiles[(x, y)] = firstMatch; - x++; - } - else - { - x = 0; - y++; - } - } - - var gridWidth = placedTiles.Keys.Max(t => t.x) + 1; - var gridHeight = placedTiles.Keys.Max(t => t.y) + 1; - - var seaMonsterCount = Enumerable.Range(0, gridWidth * 8 - seaMonsterWidth) - .SelectMany(_ => Enumerable.Range(0, gridHeight * 8 - seaMonsterHeight), (i, j) => (i, j)) - .Count(HasSeaMonster); - - if (seaMonsterCount == 0) return 0; - - var roughness = 0; - for (var j = 0; j < gridHeight; j++) - for (var i = 0; i < gridWidth; i++) - if (Grid(x, y) == '#') - roughness++; - - return roughness - seaMonsterCount * seaMonsterTiles; + angleSides.ExceptWith(connectionsSides); + return angle.RotateUntilSidesCorrespondToTopLeft(angleSides); } - public override string Part1() => - $"{_topLefts.Select(t => t.TileId).Distinct().Aggregate(1L, (acc, next) => acc * next)}"; - - public override string Part2() => - $"{_topLefts.Select(Roughness).First(r => r > 0)}"; - - private record Tile(int TileId, char[][] Pixels) + private static int TagSeaMonsters(char[][] lines) { - private const int Size = 10; - internal int TopId => GetId(z => (z, 0)); - internal int BottomId => GetId(z => (z, Size - 1)); - internal int LeftId => GetId(z => (0, z)); - internal int RightId => GetId(z => (Size - 1, z)); + var images = new List { lines }; + lines = lines.Rotate(); + images.Add(lines); + lines = lines.Rotate(); + images.Add(lines); + lines = lines.Rotate(); + images.Add(lines); + lines = lines.FlipHorizontally(); + images.Add(lines); + lines = lines.Rotate(); + images.Add(lines); + lines = lines.Rotate(); + images.Add(lines); + lines = lines.Rotate(); + images.Add(lines); - private int GetId(Func selector) => Enumerable.Range(0, Size) - .Select(selector) - .Select((c, i) => (Pixels[c.x][c.y] == '#' ? 1 : 0) << i) - .Aggregate(0, (acc, next) => acc | next); - - private Tile RotateClockwise() => Transform((x, y, newPixels) => newPixels[x][Size - 1 - y] = Pixels[y][x]); - - private Tile Flip() => Transform((x, y, newPixels) => newPixels[y][Size - 1 - x] = Pixels[y][x]); - - private Tile Transform(Action transformFunc) - { - var newPixels = Enumerable.Repeat(false, Size).Select(_ => new char[Size]).ToArray(); - - for (var y = 0; y < Size; y++) - for (var x = 0; x < Size; x++) - transformFunc(x, y, newPixels); - - return new(TileId, newPixels); - } - - internal IEnumerable AllPermutations() - { - var tile = this; - for (var i = 1; i <= 8; i++) - { - yield return tile; - if (i == 4) tile = Flip(); - else if (i == 8) yield break; - tile = tile.RotateClockwise(); - } - } - - public string Format() => $"Tile {TileId}:\n{string.Join("\n", Pixels.Select(p => new string(p)))}"; + return images.Select(CountSeaMonstersInImage).Sum(); } -} + + private static int CountSeaMonstersInImage(char[][] lines) + { + const string pattern = @"(?<=#.{77})#.{4}#{2}.{4}#{2}.{4}#{3}(?=.{77}#.{2}#.{2}#.{2}#.{2}#.{2}#)"; + var singleLine = lines.Aggregate("", (curr, next) => curr + new string(next)); + var matches = Regex.Matches(singleLine, pattern); + return matches.Count; + } + + private class PuzzlePiece + { + public readonly long Id; + private readonly char[][] _piece; + private readonly Lazy _topSide; + public readonly Lazy RightSide; + public readonly Lazy BottomSide; + private readonly Lazy _leftSide; + public readonly Lazy Sides; + public readonly Lazy> AllSidesWithFlipped; + public readonly Lazy<(string, string)[]> SidesWithFlippedPaired; + + public static PuzzlePiece CreatePieceFromString(string[] pieceWithId) + { + var id = long.Parse(pieceWithId[0][5..^1]); + var piece = pieceWithId[1..].Select(x => x.ToCharArray()).ToArray(); + return new PuzzlePiece(id, piece); + } + + private PuzzlePiece(long id, char[][] piece) + { + Id = id; + _piece = piece; + + _topSide = new Lazy(() => new string(piece[0])); + RightSide = new Lazy(() => new string(piece.Select(line => line[^1]).ToArray())); + BottomSide = new Lazy(() => new string(piece[^1].Reverse().ToArray())); + _leftSide = new Lazy(() => new string(piece.Select(line => line[0]).Reverse().ToArray())); + Sides = new Lazy(() => new[] + { _topSide.Value, RightSide.Value, BottomSide.Value, _leftSide.Value }); + SidesWithFlippedPaired = new Lazy<(string, string)[]>(() => CalculateSidesWithFlipped(this)); + AllSidesWithFlipped = new Lazy>(() => CalculateAllSidesWithFlipped(this)); + } + + public override bool Equals(object? obj) => obj is PuzzlePiece piece && Id == piece.Id; + public override int GetHashCode() => HashCode.Combine(Id); + public override string ToString() => Id.ToString(); + + public PuzzlePiece TransformSoTopMatchesWith(string sideToMatch) => + TransformSoSideMatchesWith(new string(sideToMatch.Reverse().ToArray()), p => p._topSide.Value); + + public PuzzlePiece TransformSoLeftMatchesWith(string sideToMatch) => + TransformSoSideMatchesWith(new string(sideToMatch.Reverse().ToArray()), p => p._leftSide.Value); + + private PuzzlePiece TransformSoSideMatchesWith(string sideToMatch, Func getSide) + { + var side = getSide(this); + if (side == sideToMatch) return this; + return Sides.Value.ToHashSet().Contains(sideToMatch) + ? Rotated().TransformSoSideMatchesWith(sideToMatch, getSide) + : Flipped().TransformSoSideMatchesWith(sideToMatch, getSide); + } + + public PuzzlePiece RotateUntilSidesCorrespondToTopLeft(IReadOnlySet sides) => + sides.Contains(_leftSide.Value) && sides.Contains(_topSide.Value) + ? this + : Rotated().RotateUntilSidesCorrespondToTopLeft(sides); + + private PuzzlePiece Rotated() => new(Id, _piece.Rotate()); + + private PuzzlePiece Flipped() => new(Id, _piece.FlipHorizontally()); + + public string GetLine(int l) => new(_piece[l]); + + private static (string, string)[] CalculateSidesWithFlipped(PuzzlePiece piece) => + new (string, string)[] + { + (piece._topSide.Value, new string(piece._topSide.Value.Reverse().ToArray())), + (piece.RightSide.Value, new string(piece.RightSide.Value.Reverse().ToArray())), + (piece.BottomSide.Value, new string(piece.BottomSide.Value.Reverse().ToArray())), + (piece._leftSide.Value, new string(piece._leftSide.Value.Reverse().ToArray())), + }; + + private static HashSet CalculateAllSidesWithFlipped(PuzzlePiece piece) => + piece.Sides.Value.Concat(piece.Sides.Value.Select(s => new string(s.Reverse().ToArray()))).ToHashSet(); + } +} \ No newline at end of file diff --git a/aoc2020/Extensions.cs b/aoc2020/Extensions.cs index e488753..438f03c 100644 --- a/aoc2020/Extensions.cs +++ b/aoc2020/Extensions.cs @@ -14,7 +14,32 @@ public static class Extensions /// /// /// - public static double ScaleMilliseconds(this Stopwatch stopwatch) => 1_000 * stopwatch.ElapsedTicks / (double)Stopwatch.Frequency; + public static double ScaleMilliseconds(this Stopwatch stopwatch) => + 1_000 * stopwatch.ElapsedTicks / (double)Stopwatch.Frequency; public static bool Contains(this Range range, int i) => i >= range.Start.Value && i <= range.End.Value; + + /// + /// Given an array, it returns a rotated copy. + /// + /// The two dimensional jagged array to rotate. + public static T[][] Rotate(this T[][] array) + { + var result = new T[array[0].Length][]; + for (var i = 0; i < result.Length; i++) + result[i] = new T[array.Length]; + + for (var i = 0; i < array.Length; i++) + for (var j = 0; j < array[i].Length; j++) + result[i][j] = array[array.Length - j - 1][i]; + + return result; + } + + /// + /// Given a jagged array, it returns a diagonally flipped copy. + /// + /// The two dimensional jagged array to flip. + public static T[][] FlipHorizontally(this IEnumerable array) => + array.Select(x => x.Reverse().ToArray()).ToArray(); }