mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
261 lines
5.7 KiB
Go
261 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/alexchao26/advent-of-code-go/util"
|
|
)
|
|
|
|
//go:embed input.txt
|
|
var input string
|
|
|
|
func init() {
|
|
// do this in init (not main) so test file has same input
|
|
input = strings.TrimRight(input, "\n")
|
|
if len(input) == 0 {
|
|
panic("empty input.txt file")
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
var part int
|
|
flag.IntVar(&part, "part", 1, "part 1 or 2")
|
|
flag.Parse()
|
|
fmt.Println("Running part", part)
|
|
|
|
ans := pipeMaze(input, part)
|
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
|
fmt.Println("Output:", ans)
|
|
}
|
|
|
|
var pipes = map[string][2][2]int{
|
|
"|": {
|
|
{-1, 0}, // up
|
|
{1, 0}, // down
|
|
},
|
|
"-": {
|
|
{0, -1}, // left
|
|
{0, 1}, //right
|
|
},
|
|
"L": {
|
|
{-1, 0}, // up
|
|
{0, 1}, // right
|
|
},
|
|
"J": {
|
|
{-1, 0}, // up
|
|
{0, -1}, // left
|
|
},
|
|
"7": {
|
|
{0, -1}, // left
|
|
{1, 0}, // down
|
|
},
|
|
"F": {
|
|
{0, 1}, // right
|
|
{1, 0}, // down
|
|
},
|
|
}
|
|
|
|
func pipeMaze(input string, part int) int {
|
|
grid := parseInput(input)
|
|
|
|
var r, c int
|
|
for i, row := range grid {
|
|
for j, val := range row {
|
|
if val == "S" {
|
|
r = i
|
|
c = j
|
|
}
|
|
}
|
|
}
|
|
|
|
fillGridLocation(grid, r, c)
|
|
|
|
// traverse entire loop to determine length
|
|
loopCoords := map[[2]int]bool{}
|
|
toVisit := [][2]int{
|
|
{r, c},
|
|
}
|
|
for len(toVisit) > 0 {
|
|
coords := toVisit[0]
|
|
if loopCoords[coords] {
|
|
break
|
|
}
|
|
loopCoords[coords] = true
|
|
toVisit = toVisit[1:]
|
|
|
|
// assumes loop is well formed, will cause a panic if not
|
|
diffs := pipes[grid[coords[0]][coords[1]]]
|
|
for _, diff := range diffs {
|
|
nextRow, nextCol := coords[0]+diff[0], coords[1]+diff[1]
|
|
if isInRange(grid, nextRow, nextCol) && !loopCoords[[2]int{nextRow, nextCol}] {
|
|
toVisit = append(toVisit, [2]int{nextRow, nextCol})
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if part == 1 {
|
|
return len(loopCoords) / 2
|
|
}
|
|
|
|
// part 2
|
|
|
|
// create copy of grid with all non-loop spots replaced with a period
|
|
reducedGrid := make([][]string, len(grid))
|
|
for i := 0; i < len(grid); i++ {
|
|
for j := 0; j < len(grid[0]); j++ {
|
|
if loopCoords[[2]int{i, j}] {
|
|
reducedGrid[i] = append(reducedGrid[i], grid[i][j])
|
|
} else {
|
|
reducedGrid[i] = append(reducedGrid[i], ".")
|
|
}
|
|
}
|
|
}
|
|
|
|
// expand grid to double plus 2 in both dimensions to account for squeezing between pipes
|
|
// the plus two is to add an empty row/column on each side for easier traversing from the outside
|
|
expandedGrid := [][]string{}
|
|
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
|
|
|
|
for r, rows := range reducedGrid {
|
|
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
|
|
for c, val := range rows {
|
|
expandedGrid[r*2+1][c*2+1] = val
|
|
}
|
|
// empty row
|
|
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
|
|
}
|
|
|
|
// fill gaps between loop coords so we have an encased area again
|
|
// we can naively try to fill in every empty spot because only ones with two valid connecting
|
|
// pipes will be filled
|
|
for r, rows := range expandedGrid {
|
|
for c, val := range rows {
|
|
if val == "" {
|
|
fillGridLocation(expandedGrid, r, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// replacing empty strings with spaces makes the printout human readable
|
|
for r, rows := range expandedGrid {
|
|
for c, val := range rows {
|
|
if val == "" {
|
|
expandedGrid[r][c] = " "
|
|
}
|
|
}
|
|
}
|
|
|
|
toVisit = [][2]int{
|
|
{0, 0},
|
|
}
|
|
seen := map[[2]int]bool{}
|
|
for len(toVisit) > 0 {
|
|
coords := toVisit[0]
|
|
toVisit = toVisit[1:]
|
|
if seen[coords] {
|
|
continue
|
|
}
|
|
seen[coords] = true
|
|
|
|
// delete reachable dots
|
|
if expandedGrid[coords[0]][coords[1]] == "." {
|
|
expandedGrid[coords[0]][coords[1]] = " "
|
|
}
|
|
|
|
for _, diff := range [][2]int{
|
|
{-1, 0},
|
|
{1, 0},
|
|
{0, -1},
|
|
{0, 1},
|
|
} {
|
|
nextRow := coords[0] + diff[0]
|
|
nextCol := coords[1] + diff[1]
|
|
if isInRange(expandedGrid, nextRow, nextCol) {
|
|
if expandedGrid[nextRow][nextCol] == "." || expandedGrid[nextRow][nextCol] == " " {
|
|
toVisit = append(toVisit, [2]int{nextRow, nextCol})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// count remaining dots
|
|
var ans int
|
|
for _, rows := range expandedGrid {
|
|
for _, val := range rows {
|
|
if val == "." {
|
|
ans++
|
|
}
|
|
}
|
|
}
|
|
|
|
// for _, rows := range expandedGrid {
|
|
// fmt.Println(rows)
|
|
// }
|
|
|
|
return ans
|
|
}
|
|
|
|
func fillGridLocation(grid [][]string, r, c int) {
|
|
// inputs are nice and exactly two adjacent cells that connect to S
|
|
// check four directions from start
|
|
leftCol := c - 1
|
|
rightCol := c + 1
|
|
upRow := r - 1
|
|
downRow := r + 1
|
|
|
|
var combinedString string
|
|
// check left for inRange and possible valid pipe types
|
|
if isInRange(grid, r, leftCol) &&
|
|
(grid[r][leftCol] == "-" || grid[r][leftCol] == "L" || grid[r][leftCol] == "F") {
|
|
combinedString += "left"
|
|
}
|
|
// right
|
|
if isInRange(grid, r, rightCol) &&
|
|
(grid[r][rightCol] == "-" || grid[r][rightCol] == "J" || grid[r][rightCol] == "7") {
|
|
combinedString += "right"
|
|
}
|
|
// up
|
|
if isInRange(grid, upRow, c) &&
|
|
(grid[upRow][c] == "|" || grid[upRow][c] == "7" || grid[upRow][c] == "F") {
|
|
combinedString += "up"
|
|
}
|
|
if isInRange(grid, downRow, c) &&
|
|
(grid[downRow][c] == "|" || grid[downRow][c] == "J" || grid[downRow][c] == "L") {
|
|
combinedString += "down"
|
|
}
|
|
|
|
switch combinedString {
|
|
case "leftup":
|
|
grid[r][c] = "J"
|
|
case "leftdown":
|
|
grid[r][c] = "7"
|
|
case "rightup":
|
|
grid[r][c] = "L"
|
|
case "rightdown":
|
|
grid[r][c] = "F"
|
|
case "leftright":
|
|
grid[r][c] = "-"
|
|
case "updown":
|
|
grid[r][c] = "|"
|
|
// default:
|
|
// do not panic so we can use this function more naively for the expanded grid
|
|
// could return an error instead and choose to check it for part1 where we NEED it to find a result
|
|
// panic("ineligible configuration: " + combinedString)
|
|
}
|
|
}
|
|
|
|
func isInRange(grid [][]string, row, col int) bool {
|
|
return row >= 0 && row < len(grid) && col >= 0 && col < len(grid[0])
|
|
}
|
|
|
|
func parseInput(input string) (ans [][]string) {
|
|
for _, line := range strings.Split(input, "\n") {
|
|
ans = append(ans, strings.Split(line, ""))
|
|
}
|
|
return ans
|
|
}
|