diff --git a/README.md b/README.md index c2d374c..590d469 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Advent of Code 2023 [![Tests](https://github.com/devries/advent_of_code_2023/actions/workflows/main.yml/badge.svg)](https://github.com/devries/advent_of_code_2023/actions/workflows/main.yml) -[![Stars: 34](https://img.shields.io/badge/⭐_Stars-34-yellow)](https://adventofcode.com/2023) +[![Stars: 36](https://img.shields.io/badge/⭐_Stars-36-yellow)](https://adventofcode.com/2023) ## Plan for This Year @@ -282,3 +282,16 @@ the third run of my solution after compilation on my Raspberry Pi. state to position and direction. For neighboring states, I assume I will turn 90 degrees and find all states 4 to 10 steps away in each direction. This runs much faster as shown above. + +- [Day 18: Lavaduct Lagoon](https://adventofcode.com/2023/day/18) - [⭐ part 1](day18p1/solution.go), [⭐ part 2](day18p2/solution.go) + + For the first part I did a flood fill of points outside the trench and then + subtracted the area not filled from the surrounding rectangle. I should have + seen the second part coming, but essentially what I did was find all the + horizontal line segments in the trenches. I then worked up from the lowest + segment to the highest segment calculating the area of rectangles above the + existing segments until they intersect other segments. I did the accounting + in a complicated and error prone way which meant that I was able to only + finish the first part before work and had to wait to finish the second + part until after work. I think there has to be a more elegant way to express + what I was trying to express, but I didn't find it today. diff --git a/day18p1/solution.go b/day18p1/solution.go new file mode 100644 index 0000000..ae4dd12 --- /dev/null +++ b/day18p1/solution.go @@ -0,0 +1,142 @@ +package day18p1 + +import ( + "fmt" + "io" + "regexp" + "strconv" + + "aoc/utils" +) + +func Solve(r io.Reader) any { + lines := utils.ReadLines(r) + + re := regexp.MustCompile(`([RDLU])\s+(\d+)\s+\(#([0-9a-f]+)\)`) + + grid := make(map[utils.Point]bool) + pos := utils.Point{} + grid[pos] = true + xmin, ymin, xmax, ymax := 0, 0, 0, 0 + + for _, ln := range lines { + sm := re.FindStringSubmatch(ln) + + inst := Instruction{} + switch sm[1] { + case "U": + inst.Direction = utils.North + case "D": + inst.Direction = utils.South + case "R": + inst.Direction = utils.East + case "L": + inst.Direction = utils.West + default: + panic("Direction not good") + } + + var err error + inst.Distance, err = strconv.Atoi(sm[2]) + if err != nil { + utils.Check(err, "Unable to convert %s to int", sm[2]) + } + + inst.Color = sm[3] + + // dig out trenches + for i := 0; i < inst.Distance; i++ { + pos = pos.Add(inst.Direction) + if pos.X > xmax { + xmax = pos.X + } + if pos.Y > ymax { + ymax = pos.Y + } + if pos.X < xmin { + xmin = pos.X + } + if pos.Y < ymin { + ymin = pos.Y + } + + grid[pos] = true + } + } + + if utils.Verbose { + printGrid(grid, xmin, ymin, xmax, ymax) + } + + g := &Grid{grid, xmin, ymin, xmax, ymax} + + bfs := utils.NewBFS[utils.Point]() + + _, err := bfs.Run(g) + if err != utils.BFSNotFound { + panic("did not exhaust grid") + } + + outside := len(bfs.Visited) + + if utils.Verbose { + printGrid(bfs.Visited, xmin-1, ymin-1, xmax+1, ymax+1) + } + + area := (xmax - xmin + 3) * (ymax - ymin + 3) + return area - outside +} + +type Instruction struct { + Direction utils.Point + Distance int + Color string +} + +func printGrid(grid map[utils.Point]bool, xmin, ymin, xmax, ymax int) { + for j := ymax; j >= ymin; j-- { + for i := xmin; i <= xmax; i++ { + switch grid[utils.Point{X: i, Y: j}] { + case true: + fmt.Printf("#") + case false: + fmt.Printf(".") + } + } + fmt.Printf("\n") + } + fmt.Printf("\n") +} + +type Grid struct { + Edges map[utils.Point]bool + Xmin int + Ymin int + Xmax int + Ymax int +} + +func (g *Grid) GetInitial() utils.Point { + return utils.Point{X: g.Xmin - 1, Y: g.Ymin - 1} +} + +func (g *Grid) GetNeighbors(p utils.Point) []utils.Point { + ret := []utils.Point{} + + for _, dir := range utils.Directions { + np := p.Add(dir) + + if np.X > g.Xmax+1 || np.X < g.Xmin-1 || np.Y < g.Ymin-1 || np.Y > g.Ymax+1 { + continue + } + if !g.Edges[np] { + ret = append(ret, np) + } + } + + return ret +} + +func (g *Grid) IsFinal(p utils.Point) bool { + return false +} diff --git a/day18p1/solution_test.go b/day18p1/solution_test.go new file mode 100644 index 0000000..8223341 --- /dev/null +++ b/day18p1/solution_test.go @@ -0,0 +1,46 @@ +package day18p1 + +import ( + "strings" + "testing" + + "aoc/utils" +) + +var testInput = `R 6 (#70c710) +D 5 (#0dc571) +L 2 (#5713f0) +D 2 (#d2c081) +R 2 (#59c680) +D 2 (#411b91) +L 5 (#8ceee2) +U 2 (#caa173) +L 1 (#1b58a2) +U 2 (#caa171) +R 2 (#7807d2) +U 3 (#a77fa3) +L 2 (#015232) +U 2 (#7a21e3)` + +func TestSolve(t *testing.T) { + tests := []struct { + input string + answer int + }{ + {testInput, 62}, + } + + if testing.Verbose() { + utils.Verbose = true + } + + for _, test := range tests { + r := strings.NewReader(test.input) + + result := Solve(r).(int) + + if result != test.answer { + t.Errorf("Expected %d, got %d", test.answer, result) + } + } +} diff --git a/day18p2/solution.go b/day18p2/solution.go new file mode 100644 index 0000000..39a0a9e --- /dev/null +++ b/day18p2/solution.go @@ -0,0 +1,216 @@ +package day18p2 + +import ( + "io" + "regexp" + "sort" + "strconv" + + "aoc/utils" +) + +func Solve(r io.Reader) any { + lines := utils.ReadLines(r) + + re := regexp.MustCompile(`([RDLU])\s+(\d+)\s+\(#([0-9a-f]+)\)`) + + pos := utils.Point{} + xmin, ymin, xmax, ymax := 0, 0, 0, 0 + segments := SegmentList{} + + for _, ln := range lines { + sm := re.FindStringSubmatch(ln) + + inst := Instruction{} + actualDirection := []rune(sm[3])[5] + switch actualDirection { + case '3': + inst.Direction = utils.North + case '1': + inst.Direction = utils.South + case '0': + inst.Direction = utils.East + case '2': + inst.Direction = utils.West + default: + panic("Direction not good") + } + + distHex := string([]rune(sm[3])[:5]) + dist, err := strconv.ParseInt(distHex, 16, 32) + if err != nil { + utils.Check(err, "Unable to convert %s to int", sm[2]) + } + inst.Distance = int(dist) + + newpos := pos.Add(inst.Direction.Scale(inst.Distance)) + + // We'll be collecting horizontal segments and calculating + // area by moving up from ymin to ymax. + if inst.Direction == utils.East { + segment := HSegment{pos, newpos} + segments = append(segments, segment) + } else if inst.Direction == utils.West { + segment := HSegment{newpos, pos} + segments = append(segments, segment) + } + pos = newpos + + if pos.X > xmax { + xmax = pos.X + } + if pos.Y > ymax { + ymax = pos.Y + } + if pos.X < xmin { + xmin = pos.X + } + if pos.Y < ymin { + ymin = pos.Y + } + } + + // There has got to be a simpler way to do whatever it is I do below, but I + // was just not getting it today. + + // Sort all line segments by y and x, so we iterate from + // lower left to upper right. + sort.Sort(segments) + ycurrent := ymin + + // Current ranges are segments over which we are calculating a filled in area. + currentRanges := []int{} + + // Last motion ranges are the ranges over which we last caulculated filled in areas. + lastMotionRanges := []int{} + + // This is the last border region calculated, which may be discarded if there has + // been no vertical motion. + lastBorderArea := uint64(0) + + var area uint64 + for _, s := range segments { + d := s.Left.Y - ycurrent - 1 // hight between end segments. Ends are calculated separately. + if d > 0 { + // There has been a change in Y so we calculate the area of rectangles + // with the current segment + for i := 0; i < len(currentRanges); i += 2 { + area += uint64(d) * uint64(currentRanges[i+1]-currentRanges[i]+1) + } + ycurrent = s.Left.Y + // Save the last segments used to calculate area + lastMotionRanges = make([]int, len(currentRanges)) + copy(lastMotionRanges, currentRanges) + } else { + // We didn't move vertically, so the last border calculation we had + // was not a complete border, remove it and recalculate with next + // segment. + area -= lastBorderArea + } + + previousRanges := make([]int, len(currentRanges)) + copy(previousRanges, currentRanges) + + // Add line segment to current ranges and find areas where areas will be + // closed off or opened up by sorting the segments and calculating the + // remaining alternating segments. + // current: x-------------x x---------------x + // added: x---------x + // final: x-----x x-x x---------------x + currentRanges = append(currentRanges, s.Left.X, s.Right.X) + sort.Ints(currentRanges) + + // Need to remove duplicate points + for i := 0; i < len(currentRanges)-1; i++ { + if currentRanges[i] == currentRanges[i+1] { + // need to remove duplicates + currentRanges = append(currentRanges[:i], currentRanges[i+2:]...) + i -= 1 + } + } + + // Since the last time there was movement find all overlap to get points in overlap + // region between the areas just added and the areas added in the next iteration + // lastMovement: x-------------x x---------------x + // current: x---------x + // overlap: x---------------x x---------------x + boundaries := BoundaryList{} + + // Use method where we add 1 for start of regions and subtract one for end + // any time sum > 0 there is at least one segment, and when sum == 0 then + // there is no segment in either set of segments here. + for i := 0; i < len(lastMotionRanges); i += 2 { + boundaries = append(boundaries, Boundary{lastMotionRanges[i], 1}) + boundaries = append(boundaries, Boundary{lastMotionRanges[i+1], -1}) + } + + for i := 0; i < len(currentRanges); i += 2 { + boundaries = append(boundaries, Boundary{currentRanges[i], 1}) + boundaries = append(boundaries, Boundary{currentRanges[i+1], -1}) + } + + sort.Sort(boundaries) + + current := 0 + combinedRanges := []int{} + + for _, b := range boundaries { + pos := b.Position + current += b.Incrementor + if current == 1 && b.Incrementor == 1 { + combinedRanges = append(combinedRanges, pos) + } + if current == 0 && b.Incrementor == -1 { + combinedRanges = append(combinedRanges, pos) + } + } + lastBorderArea = 0 + for i := 0; i < len(combinedRanges); i += 2 { + v := uint64(combinedRanges[i+1] - combinedRanges[i] + 1) + // keep the area here, because if we have another segment in this row to change + // then we will have to subtract this change and do the problem again. + lastBorderArea += v + area += v + } + } + + return area +} + +type Instruction struct { + Direction utils.Point + Distance int +} + +type HSegment struct { + Left utils.Point + Right utils.Point +} + +type SegmentList []HSegment + +func (s SegmentList) Len() int { return len(s) } +func (s SegmentList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s SegmentList) Less(i, j int) bool { + li := s[i].Left + lj := s[j].Left + + if li.Y != lj.Y { + return li.Y < lj.Y + } + + return li.X < lj.X +} + +type Boundary struct { + Position int + Incrementor int +} + +type BoundaryList []Boundary + +func (s BoundaryList) Len() int { return len(s) } +func (s BoundaryList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s BoundaryList) Less(i, j int) bool { + return s[i].Position < s[j].Position +} diff --git a/day18p2/solution_test.go b/day18p2/solution_test.go new file mode 100644 index 0000000..e5dc202 --- /dev/null +++ b/day18p2/solution_test.go @@ -0,0 +1,46 @@ +package day18p2 + +import ( + "strings" + "testing" + + "aoc/utils" +) + +var testInput = `R 6 (#70c710) +D 5 (#0dc571) +L 2 (#5713f0) +D 2 (#d2c081) +R 2 (#59c680) +D 2 (#411b91) +L 5 (#8ceee2) +U 2 (#caa173) +L 1 (#1b58a2) +U 2 (#caa171) +R 2 (#7807d2) +U 3 (#a77fa3) +L 2 (#015232) +U 2 (#7a21e3)` + +func TestSolve(t *testing.T) { + tests := []struct { + input string + answer uint64 + }{ + {testInput, 952408144115}, + } + + if testing.Verbose() { + utils.Verbose = true + } + + for _, test := range tests { + r := strings.NewReader(test.input) + + result := Solve(r).(uint64) + + if result != test.answer { + t.Errorf("Expected %d, got %d", test.answer, result) + } + } +} diff --git a/inputs b/inputs index e88f253..4bb8445 160000 --- a/inputs +++ b/inputs @@ -1 +1 @@ -Subproject commit e88f2532f10e8e3fb559214d2256505fe1646ca5 +Subproject commit 4bb8445ec52a513bae4082b5c716a362dc4a816b