diff --git a/2025/cmd/root.go b/2025/cmd/root.go index 140ee44..780d7b0 100644 --- a/2025/cmd/root.go +++ b/2025/cmd/root.go @@ -17,6 +17,7 @@ import ( "cpatino.com/advent-of-code/2025/day06" "cpatino.com/advent-of-code/2025/day07" "cpatino.com/advent-of-code/2025/day08" + "cpatino.com/advent-of-code/2025/day09" ) var days = map[int]func(string) (any, any){ @@ -28,6 +29,7 @@ var days = map[int]func(string) (any, any){ 6: day06.Run, 7: day07.Run, 8: day08.Run, + 9: day09.Run, } var rootCmd = &cobra.Command{ diff --git a/2025/day09/day09.go b/2025/day09/day09.go new file mode 100755 index 0000000..1f2dc29 --- /dev/null +++ b/2025/day09/day09.go @@ -0,0 +1,242 @@ +package day09 + +import ( + "bufio" + "fmt" + "os" + "runtime" + "sync" +) + +type Point struct { + x, y int +} + +var tiles []Point + +func Part1(tiles []Point) int { + n := len(tiles) + if n < 2 { + return 0 + } + + numWorkers := runtime.NumCPU() + maxChan := make(chan int, numWorkers) + var wg sync.WaitGroup + + chunkSize := (n + numWorkers - 1) / numWorkers + + for w := range numWorkers { + start := w * chunkSize + end := min(start+chunkSize, n) + wg.Add(1) + + go func(start, end int) { + defer wg.Done() + + localMax := 0 + for i := start; i < end; i++ { + for j := i + 1; j < n; j++ { + t1 := tiles[i] + t2 := tiles[j] + area := (abs(t1.x-t2.x) + 1) * (abs(t1.y-t2.y) + 1) + if area > localMax { + localMax = area + } + } + } + + maxChan <- localMax + }(start, end) + } + + wg.Wait() + close(maxChan) + + maxArea := 0 + for v := range maxChan { + if v > maxArea { + maxArea = v + } + } + return maxArea +} + +func Part2(tiles []Point) int { + if len(tiles) < 2 { + return 0 + } + + red := make(map[Point]bool, len(tiles)) + for _, p := range tiles { + red[p] = true + } + + green := make(map[Point]bool) + n := len(tiles) + for i := range n { + a := tiles[i] + b := tiles[(i+1)%n] + if a.x == b.x { + sy, ey := a.y, b.y + if sy > ey { + sy, ey = ey, sy + } + for y := sy + 1; y < ey; y++ { + pt := Point{a.x, y} + if !red[pt] { + green[pt] = true + } + } + } else if a.y == b.y { + sx, ex := a.x, b.x + if sx > ex { + sx, ex = ex, sx + } + for x := sx + 1; x < ex; x++ { + pt := Point{x, a.y} + if !red[pt] { + green[pt] = true + } + } + } + } + + type vedge struct{ x, y1, y2 int } + var vedges []vedge + for i := range n { + a := tiles[i] + b := tiles[(i+1)%n] + if a.x == b.x { + sy, ey := a.y, b.y + if sy > ey { + sy, ey = ey, sy + } + vedges = append(vedges, vedge{a.x, sy, ey}) + } + } + + isInside := func(p Point) bool { + cross := 0 + for _, e := range vedges { + if p.y > e.y1 && p.y <= e.y2 { // edge crosses scanline at p.y + if e.x > p.x { + cross++ + } + } + } + return cross%2 == 1 + } + if n < 2 { + return 0 + } + + numWorkers := runtime.NumCPU() + maxChan := make(chan int, numWorkers) + var wg sync.WaitGroup + + chunkSize := (n + numWorkers - 1) / numWorkers + + for w := range numWorkers { + start := w * chunkSize + end := start + chunkSize + if end > n { + end = n + } + wg.Add(1) + go func(start, end int) { + defer wg.Done() + localMax := 0 + for i := start; i < end; i++ { + for j := i + 1; j < n; j++ { + t1, t2 := tiles[i], tiles[j] + sx, ex := t1.x, t2.x + if sx > ex { + sx, ex = ex, sx + } + + sy, ey := t1.y, t2.y + if sy > ey { + sy, ey = ey, sy + } + + valid := true + for x := sx; x <= ex && valid; x++ { + for _, y := range []int{sy, ey} { + pt := Point{x, y} + if !red[pt] && !green[pt] && !isInside(pt) { + valid = false + break + } + } + } + + for y := sy + 1; y < ey && valid; y++ { + for _, x := range []int{sx, ex} { + pt := Point{x, y} + if !red[pt] && !green[pt] && !isInside(pt) { + valid = false + break + } + } + } + + if valid { + area := (ex - sx + 1) * (ey - sy + 1) + if area > localMax { + localMax = area + } + } + } + } + maxChan <- localMax + }(start, end) + } + + wg.Wait() + close(maxChan) + + maxArea := 0 + for v := range maxChan { + if v > maxArea { + maxArea = v + } + } + return maxArea +} + +func abs(a int) int { + if a < 0 { + return -a + } + return a +} + +func Run(filename string) (any, any) { + file, err := os.Open(filename) + if err != nil { + panic(err) + } + defer file.Close() + + tiles = nil + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + var x, y int + _, err := fmt.Sscanf(line, "%d,%d", &x, &y) + if err != nil { + panic(fmt.Sprintf("failed to parse line: %q: %v", line, err)) + } + tiles = append(tiles, Point{x, y}) + } + + if err := scanner.Err(); err != nil { + panic(err) + } + + part1 := Part1(tiles) + part2 := Part2(tiles) + + return part1, part2 +} diff --git a/2025/day09/day09_test.go b/2025/day09/day09_test.go new file mode 100755 index 0000000..aa2b8f8 --- /dev/null +++ b/2025/day09/day09_test.go @@ -0,0 +1,34 @@ +package day09 + +import ( + "os" + "testing" +) + +type TestCase struct { + FileName string + Part1 any + Part2 any +} + +func TestDay09(t *testing.T) { + tests := []TestCase{ + {"test.txt", 50, 24}, + {"input.txt", 4777824480, 1542119040}, + } + + for _, test := range tests { + if _, err := os.Stat(test.FileName); os.IsNotExist(err) { + t.Fatalf("test file does not exist: %v", test.FileName) + } + + part1, part2 := Run(test.FileName) + if test.Part1 != nil && part1 != test.Part1 { + t.Fatalf("%s: unexpected part1:\nwant:\t%v\ngot:\t%v", test.FileName, test.Part1, part1) + } + + if test.Part2 != nil && part2 != test.Part2 { + t.Fatalf("%s: unexpected part2:\nwant:\t%v\ngot:\t%v", test.FileName, test.Part2, part2) + } + } +} diff --git a/2025/day09/test.txt b/2025/day09/test.txt new file mode 100755 index 0000000..c8563ea --- /dev/null +++ b/2025/day09/test.txt @@ -0,0 +1,8 @@ +7,1 +11,1 +11,7 +9,7 +9,5 +2,5 +2,3 +7,3