diff --git a/advent-of-code-2021.csproj b/advent-of-code-2021.csproj index 27831fd..051fe34 100644 --- a/advent-of-code-2021.csproj +++ b/advent-of-code-2021.csproj @@ -84,6 +84,9 @@ PreserveNewest + + PreserveNewest + diff --git a/inputs/23.txt b/inputs/23.txt new file mode 100644 index 0000000..3b25139 --- /dev/null +++ b/inputs/23.txt @@ -0,0 +1,5 @@ +############# +#...........# +###A#D#B#C### + #B#C#D#A# + ######### \ No newline at end of file diff --git a/src/23.cs b/src/23.cs new file mode 100644 index 0000000..d41f234 --- /dev/null +++ b/src/23.cs @@ -0,0 +1,555 @@ +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace aoc2021; + +internal class Day23 : Day +{ + private static bool IsPart2 = false; + + [DebuggerDisplay("Cost: {Cost}, Location: {Location}, Moves: {Moves} (Spent cost: {SpentCost})")] + class Amphipod : IEquatable + { + public int Cost; + public int Location; + public int Moves = 0; + + public int SpentCost => Cost * Moves; + + public Amphipod() + { + + } + + public Amphipod(int cost, int location) + { + Cost = cost; + Location = location; + } + + public Amphipod(Amphipod other) + { + Cost = other.Cost; + Location = other.Location; + Moves = other.Moves; + } + + public override int GetHashCode() => HashCode.Combine(Cost, Location, Moves); + + public bool Equals(Amphipod? other) => Cost == other?.Cost && Location == other.Location && Moves == other.Moves; + + public bool IsHome() + { + var isInHomeSlot = !IsPart2 ? costToHomeMap[Cost].Contains(Location) : costToHomeMapP2[Cost].Contains(Location); + return isInHomeSlot && (Moves > 0 || Location > (!IsPart2 ? 4 : 12)); + } + + public char DebugChar() => Cost switch + { + 1 => 'A', + 10 => 'B', + 100 => 'C', + _ => 'D', + }; + + public override bool Equals(object? obj) => Equals(obj as Amphipod); + } + + internal override void Go() + { + var lines = Util.ReadAllLines("inputs/23.txt"); + int numFound = 0; + int hallLen = 0; + List amps = new(); + foreach (var line in lines) + { + foreach (var ch in line) + { + if (ch >= 'A' && ch <= 'D') + { + amps.Add(new Amphipod + { + Cost = ch switch + { + 'A' => 1, + 'B' => 10, + 'C' => 100, + _ => 1000, + }, + Location = numFound + 1, + }); + numFound++; + } + else if (ch == '.') + { + hallLen++; + } + } + } + Part1(CopyAmphipods(amps)); + + IsPart2 = true; + amps.ForEach(x => + { + if (x.Location > 4) + { + x.Location += 8; + } + }); + amps.Insert(4, new Amphipod(1000, 5)); + amps.Insert(5, new Amphipod(100, 6)); + amps.Insert(6, new Amphipod(10, 7)); + amps.Insert(7, new Amphipod(1, 8)); + amps.Insert(8, new Amphipod(1000, 9)); + amps.Insert(9, new Amphipod(10, 10)); + amps.Insert(10, new Amphipod(1, 11)); + amps.Insert(11, new Amphipod(100, 12)); + Part2(CopyAmphipods(amps)); + } + + private static readonly int[] validHallStops = new int[] + { + -1, + -2, + -4, + -6, + -8, + -10, + -11, + }; + + private static readonly Dictionary roomExits = new() + { + { 1, -3 }, + { 2, -5 }, + { 3, -7 }, + { 4, -9 }, + { 5, -3 }, + { 6, -5 }, + { 7, -7 }, + { 8, -9 }, + }; + + private static readonly Dictionary roomExitsP2 = new() + { + { 1, -3 }, + { 2, -5 }, + { 3, -7 }, + { 4, -9 }, + { 5, -3 }, + { 6, -5 }, + { 7, -7 }, + { 8, -9 }, + { 9, -3 }, + { 10, -5 }, + { 11, -7 }, + { 12, -9 }, + { 13, -3 }, + { 14, -5 }, + { 15, -7 }, + { 16, -9 }, + }; + + private static Dictionary GetRoomExits(ICollection amps) + { + if (amps.Count == 8) + { + return roomExits; + } + + return roomExitsP2; + } + + private static readonly Dictionary> costToHomeMap = new() + { + { 1, new List(){ 5, 1 } }, + { 10, new List(){ 6, 2 } }, + { 100, new List(){ 7, 3 } }, + { 1000, new List(){ 8, 4 } }, + }; + + private static readonly Dictionary> costToHomeMapP2 = new() + { + { 1, new List(){ 13, 9, 5, 1 } }, + { 10, new List(){ 14, 10, 6, 2 } }, + { 100, new List(){ 15, 11, 7, 3 } }, + { 1000, new List(){ 16, 12, 8, 4 } }, + }; + + private static Dictionary> GetCostToHomeMap(ICollection amps) + { + if (amps.Count == 8) + { + return costToHomeMap; + } + + return costToHomeMapP2; + } + + private static bool CanMoveTo(ICollection amps, Amphipod mover, int target) + { + // into hall + if (mover.Moves == 0) + { + if (target > 0) + { + throw new Exception(); + } + + for (int i = target - 4; i >= 0; i -= 4) + { + if (amps.Any(x => x.Location == i)) + { + return false; + } + } + + var max = Math.Max(GetRoomExits(amps)[mover.Location], target); + var min = Math.Min(GetRoomExits(amps)[mover.Location], target); + if (amps.Any(x => x.Location >= min && x.Location <= max)) + { + return false; + } + + return true; + } + // into home + else if (mover.Location < 0) + { + if (target < 0) + { + throw new Exception(); + } + + var hallDest = GetRoomExits(amps)[target]; + var max = Math.Max(hallDest, mover.Location); + var min = Math.Min(hallDest, mover.Location); + if (amps.Any(x => x != mover && x.Location >= min && x.Location <= max)) + { + return false; + } + + for (int i = target; i > 0; i -= 4) + { + if (amps.Any(x => x.Location == i)) + { + return false; + } + } + + for (int i = target + 4; i <= amps.Count; i += 4) + { + if (!amps.Any(x => x.Location == i) || amps.Any(x => x.Cost != mover.Cost && x.Location == i)) + { + return false; + } + } + + if (!GetCostToHomeMap(amps)[mover.Cost].Contains(target)) + { + return false; + } + + return !amps.Any(x => x.Location == target); + } + + throw new Exception(); + } + + private static int GetCostTo(int loc, int target) + { + int cost = 1; + // hall to room + if (loc < 0 && target > 0) + { + if (target > 4) + { + cost++; + } + if (target > 8) + { + cost++; + } + if (target > 12) + { + cost++; + } + var hallDest = !IsPart2 ? roomExits[target] : roomExitsP2[target]; + cost += Math.Abs(hallDest - loc); + } + // room to hall + else if (loc > 0 && target < 0) + { + if (loc > 4) + { + cost++; + } + if (loc > 8) + { + cost++; + } + if (loc > 12) + { + cost++; + } + + cost += Math.Abs((!IsPart2 ? roomExits[loc] : roomExitsP2[loc]) - target); + } + + return cost; + } + + private static List CopyAmphipods(ICollection amps) + { + var copied = new List(amps.Count); + foreach (var amp in amps) + { + copied.Add(new Amphipod(amp)); + } + return copied; + } + + private static List MoveTo(IList amps, Amphipod mover, int target) + { + int currIdx = amps.IndexOf(mover); + var copied = CopyAmphipods(amps); + + var copiedAmp = copied[currIdx]; + copiedAmp.Moves += GetCostTo(copiedAmp.Location, target); + copiedAmp.Location = target; + + return copied; + } + + private static bool IsSolved(IEnumerable amps) => amps.All(x => x.IsHome()); + + private static int TotalCost(IEnumerable amps) => amps.Sum(x => x.SpentCost); + + private static long NumSolves = 0; + private static long NumAttempts = 0; + private static int LowestCost = int.MaxValue; + private static long Universe = 0; + + private static readonly ConcurrentDictionary cachedCases = new(); + + private static bool Solve(IList amps) + { + var ampHash = GetHash(amps); + if (cachedCases.TryGetValue(ampHash, out bool success)) + { + return success; + } + + if (IsSolved(amps)) + { + NumSolves++; + var solveCost = TotalCost(amps); + if (solveCost < LowestCost) + { + LowestCost = solveCost; + } + cachedCases[ampHash] = true; + Universe--; + return true; + } + + NumAttempts++; + Universe++; + + List tasks = new(); + int lastRowStart = amps.Count - 4 + 1; + var eligibleMovers = amps.Where(x => (x.Location < 0 && x.Moves > 0) || (x.Location > 0 && x.Moves == 0 && (x.Location <= 4 || !amps.Any(y => y.Location == x.Location - 4)) && (x.Location < lastRowStart || !x.IsHome()))); + foreach (var mover in eligibleMovers) + { + if (mover.Moves == 0) + { + foreach (var option in validHallStops) + { + if (CanMoveTo(amps, mover, option)) + { + var task = () => + { + var copied = MoveTo(amps, mover, option); + if (Solve(copied)) + { + cachedCases[ampHash] = true; + } + }; + if (Universe == 1) + { + tasks.Add(Task.Run(task)); + } + else + { + task(); + } + } + } + } + else if (mover.Location < 0) + { + foreach (var option in GetCostToHomeMap(amps)[mover.Cost]) + { + if (CanMoveTo(amps, mover, option)) + { + var task = () => + { + var copied = MoveTo(amps, mover, option); + if (Solve(copied)) + { + cachedCases[ampHash] = true; + } + }; + if (Universe == 1) + { + tasks.Add(Task.Run(task)); + } + else + { + task(); + } + } + } + } + } + + Task.WaitAll(tasks.ToArray()); + + cachedCases[ampHash] = false; + Universe--; + return false; + } + + private static int GetHash(IEnumerable amps) + { + int hash = 0; + foreach (var amp in amps) + { + hash = HashCode.Combine(hash, amp); + } + return hash; + } + + // i know this could be much better. i just needed something quick and dirty. + private static void Draw(IEnumerable amps) + { + var line = string.Empty; + for (int i = 0; i < 13; i++) + { + line += '█'; + } + Logger.Log(line); + + line = string.Empty; + for (int i = 0; i < 13; i++) + { + if (i == 0 || i == 12) + { + line += '█'; + } + else + { + var here = amps.FirstOrDefault(x => x.Location == -i); + line += here?.DebugChar() ?? '.'; + } + } + Logger.Log(line); + + line = string.Empty; + for (int i = 0; i < 13; i++) + { + if (i == 3 || i == 5 || i == 7 || i == 9) + { + var here = amps.FirstOrDefault(x => x.Location == (i - 1) / 2); + line += here?.DebugChar() ?? '.'; + } + else + { + line += '█'; + } + } + Logger.Log(line); + + line = string.Empty; + for (int i = 0; i < 13; i++) + { + if (i == 3 || i == 5 || i == 7 || i == 9) + { + var here = amps.FirstOrDefault(x => x.Location == 4 + ((i - 1) / 2)); + line += here?.DebugChar() ?? '.'; + } + else + { + line += '█'; + } + } + Logger.Log(line); + + if (amps.Count() > 8) + { + line = string.Empty; + for (int i = 0; i < 13; i++) + { + if (i == 3 || i == 5 || i == 7 || i == 9) + { + var here = amps.FirstOrDefault(x => x.Location == 8 + ((i - 1) / 2)); + line += here?.DebugChar() ?? '.'; + } + else + { + line += '█'; + } + } + Logger.Log(line); + + line = string.Empty; + for (int i = 0; i < 13; i++) + { + if (i == 3 || i == 5 || i == 7 || i == 9) + { + var here = amps.FirstOrDefault(x => x.Location == 12 + ((i - 1) / 2)); + line += here?.DebugChar() ?? '.'; + } + else + { + line += '█'; + } + } + Logger.Log(line); + } + + line = string.Empty; + for (int i = 0; i < 13; i++) + { + line += '█'; + } + Logger.Log(line); + } + + private static void Part1(List amps) + { + Draw(amps); + using var t = new Timer(); + + Solve(amps); + + t.Stop(); + Logger.Log($"<+black>> part1: in {NumAttempts:N0} universes, found {NumSolves:N0} solves, and the lowest cost was <+white>{LowestCost}"); + } + + private static void Part2(List amps) + { + NumSolves = 0; + NumAttempts = 0; + LowestCost = int.MaxValue; + Universe = 0; + cachedCases.Clear(); + Draw(amps); + using var t = new Timer(); + + Solve(amps); + + t.Stop(); + Logger.Log($"<+black>> part2: in {NumAttempts:N0} universes, found {NumSolves:N0} solves, and the lowest cost was <+white>{LowestCost}"); + } +} diff --git a/src/main.cs b/src/main.cs index 33534e7..be74a7c 100644 --- a/src/main.cs +++ b/src/main.cs @@ -20,7 +20,7 @@ else Day? day = null; if (string.IsNullOrEmpty(arg)) { - day = new Day22(); + day = new Day23(); } else {