501 lines
11 KiB
Go
501 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
|
|
"../../"
|
|
)
|
|
|
|
var tWidth, tHeight int
|
|
|
|
func main() {
|
|
fileNm := aoc.GetArgNumber(1)
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: ./day24 <mazefile>")
|
|
os.Exit(1)
|
|
}
|
|
var f *Floor
|
|
var poiString string
|
|
f = CreateFloorFromFile(fileNm)
|
|
for i := 0; i < len(f.pois); i++ {
|
|
poiString += f.pois[i].Name()
|
|
if f.debug {
|
|
for j := i + 1; j < len(f.pois); j++ {
|
|
idx := f.pois[i].Name() + ";" + f.pois[j].Name()
|
|
fmt.Println(idx, f.shortestPaths[idx])
|
|
}
|
|
}
|
|
}
|
|
poiPerms := aoc.StringPermutations(poiString)
|
|
shortest := -1
|
|
var shortestPerm string
|
|
for _, perm := range poiPerms {
|
|
if perm[0] != '0' {
|
|
continue
|
|
}
|
|
if aoc.ArgIsSet("-2") {
|
|
// For part 2 we return to 0
|
|
perm = perm + "0"
|
|
}
|
|
var permTtl int
|
|
for i := range perm {
|
|
if i > 0 {
|
|
beg, end := string(perm[i-1]), string(perm[i])
|
|
if beg[0] > end[0] {
|
|
beg, end = end, beg
|
|
}
|
|
permTtl += f.shortestPaths[beg+";"+end]
|
|
}
|
|
}
|
|
if permTtl < shortest || shortest == -1 {
|
|
shortestPerm = perm
|
|
shortest = permTtl
|
|
}
|
|
}
|
|
fmt.Println(shortestPerm, ": ", shortest)
|
|
}
|
|
|
|
type Floor struct {
|
|
cells []string
|
|
start *Coord
|
|
end *Coord
|
|
pois []Coord
|
|
|
|
shortestPaths map[string]int
|
|
|
|
testedPath Path
|
|
solvePath Path
|
|
|
|
cellCount int
|
|
|
|
debug bool
|
|
}
|
|
|
|
func CreateFloorFromFile(fileNm string) *Floor {
|
|
f := new(Floor)
|
|
f.debug = aoc.ArgIsSet("-d")
|
|
f.cells = aoc.FileToStringSlice(fileNm)
|
|
for y := range f.cells {
|
|
for x := range f.cells[y] {
|
|
if f.cells[y][x] != '#' {
|
|
f.cellCount++
|
|
}
|
|
if f.cells[y][x] != '#' && f.cells[y][x] != '.' {
|
|
// A point of interest
|
|
wrkCoord := NewCoord(x, y, f.cells[y][x])
|
|
f.pois = append(f.pois, *wrkCoord)
|
|
if f.cells[y][x] == '0' {
|
|
f.start = wrkCoord
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Sort the pois
|
|
sort.Sort(CoordByLabel(f.pois))
|
|
|
|
// Find the shortest paths between all points of interest
|
|
f.shortestPaths = make(map[string]int)
|
|
if aoc.ArgIsSet("-2") {
|
|
|
|
/* Output from running part 1
|
|
1;2 232
|
|
1;3 30
|
|
1;4 188
|
|
1;5 230
|
|
1;6 80
|
|
1;7 54
|
|
2;3 218
|
|
2;4 56
|
|
2;5 74
|
|
2;6 212
|
|
2;7 278
|
|
3;4 174
|
|
3;5 216
|
|
3;6 66
|
|
3;7 80
|
|
4;5 54
|
|
4;6 168
|
|
4;7 234
|
|
5;6 210
|
|
5;7 276
|
|
6;7 126
|
|
*/
|
|
|
|
f.shortestPaths["0;1"] = 30
|
|
f.shortestPaths["0;2"] = 258
|
|
f.shortestPaths["0;3"] = 56
|
|
f.shortestPaths["0;4"] = 214
|
|
f.shortestPaths["0;5"] = 256
|
|
f.shortestPaths["0;6"] = 106
|
|
f.shortestPaths["0;7"] = 44
|
|
f.shortestPaths["1;2"] = 232
|
|
f.shortestPaths["1;3"] = 30
|
|
f.shortestPaths["1;4"] = 188
|
|
f.shortestPaths["1;5"] = 230
|
|
f.shortestPaths["1;6"] = 80
|
|
f.shortestPaths["1;7"] = 54
|
|
f.shortestPaths["2;3"] = 218
|
|
f.shortestPaths["2;4"] = 56
|
|
f.shortestPaths["2;5"] = 74
|
|
f.shortestPaths["2;6"] = 212
|
|
f.shortestPaths["2;7"] = 278
|
|
f.shortestPaths["3;4"] = 174
|
|
f.shortestPaths["3;5"] = 216
|
|
f.shortestPaths["3;6"] = 66
|
|
f.shortestPaths["3;7"] = 80
|
|
f.shortestPaths["4;5"] = 54
|
|
f.shortestPaths["4;6"] = 168
|
|
//f.shortestPaths["4;7"] = 266
|
|
//f.shortestPaths["5;6"] = 262
|
|
//f.shortestPaths["5;7"] = 304
|
|
f.shortestPaths["6;7"] = 126
|
|
}
|
|
for i := range f.pois {
|
|
for j := range f.pois {
|
|
if i != j {
|
|
one, two := f.pois[i], f.pois[j]
|
|
if one.label > two.label {
|
|
one, two = two, one
|
|
}
|
|
idx := string(one.label) + ";" + string(two.label)
|
|
if _, ok := f.shortestPaths[idx]; !ok {
|
|
p := f.GetShortestPath(&one, &two)
|
|
if f.debug {
|
|
fmt.Println(p.Name())
|
|
f.PrintPath(p)
|
|
fmt.Println("Path length:", len(p.coords), "\n")
|
|
for i := range p.coords {
|
|
fmt.Print(p.coords[i].Name(), ",")
|
|
}
|
|
fmt.Println()
|
|
}
|
|
f.shortestPaths[idx] = len(p.coords) - 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f *Floor) GetPoi(lbl byte) *Coord {
|
|
for i := range f.pois {
|
|
if f.pois[i].label == lbl {
|
|
return &f.pois[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var shortestFound int
|
|
|
|
func (f *Floor) GetShortestPath(st, end *Coord) Path {
|
|
f.solvePath = *new(Path)
|
|
f.testedPath = *new(Path)
|
|
hldStart := f.start
|
|
f.start = end
|
|
f.end = st
|
|
shortestFound = -1
|
|
path, sol := f.Solve(f.start.x, f.start.y, 0)
|
|
f.start = hldStart
|
|
if sol {
|
|
return path
|
|
}
|
|
return *new(Path)
|
|
}
|
|
|
|
func (f *Floor) Print() {
|
|
for y := range f.cells {
|
|
for x := range f.cells[y] {
|
|
fmt.Print(string(f.cells[y][x]))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func (f *Floor) PrintPath(p Path) {
|
|
pathCol := color.New(color.BgGreen)
|
|
for y := range f.cells {
|
|
for x := range f.cells[y] {
|
|
if p.ContainsCoord(x, y) {
|
|
if f.cells[y][x] == '.' {
|
|
pathCol.Print("O")
|
|
} else {
|
|
pathCol.Print(string(f.cells[y][x]))
|
|
}
|
|
} else {
|
|
fmt.Print(string(f.cells[y][x]))
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func (f *Floor) IsWall(x, y int) bool {
|
|
return f.cells[y][x] == '#'
|
|
}
|
|
|
|
func (f *Floor) GetCoord(x, y int) *Coord {
|
|
return NewCoord(x, y, f.cells[y][x])
|
|
}
|
|
|
|
func (f *Floor) Solve(x, y, dist int) (Path, bool) {
|
|
var tPathContains bool
|
|
shortest := new(Path)
|
|
wrkCoord := *f.GetCoord(x, y)
|
|
wrkCoord.dist = dist
|
|
|
|
if f.end.Is(x, y) {
|
|
shortest.Append(wrkCoord)
|
|
if shortestFound == -1 || dist < shortestFound {
|
|
shortestFound = dist
|
|
}
|
|
return *shortest, true
|
|
}
|
|
if f.IsWall(x, y) || (shortestFound != -1 && dist > shortestFound) {
|
|
return *shortest, false
|
|
}
|
|
// Test if we already have this coord via a shorter path
|
|
if t := f.testedPath.GetCoordAt(x, y); t != nil {
|
|
if t.dist <= dist {
|
|
return *shortest, false
|
|
}
|
|
tPathContains = true
|
|
t.dist = dist
|
|
}
|
|
// Test if there is a shorter path to this coordinate
|
|
if !f.IsWall(x-1, y) {
|
|
if t := f.testedPath.GetCoordAt(x-1, y); t != nil {
|
|
if t.dist+1 < wrkCoord.dist {
|
|
return *shortest, false
|
|
}
|
|
}
|
|
}
|
|
if !f.IsWall(x+1, y) {
|
|
if t := f.testedPath.GetCoordAt(x+1, y); t != nil {
|
|
if t.dist+1 < wrkCoord.dist {
|
|
return *shortest, false
|
|
}
|
|
}
|
|
}
|
|
if !f.IsWall(x, y-1) {
|
|
if t := f.testedPath.GetCoordAt(x, y-1); t != nil {
|
|
if t.dist+1 < wrkCoord.dist {
|
|
return *shortest, false
|
|
}
|
|
}
|
|
}
|
|
if !f.IsWall(x, y+1) {
|
|
if t := f.testedPath.GetCoordAt(x, y+1); t != nil {
|
|
if t.dist+1 < wrkCoord.dist {
|
|
return *shortest, false
|
|
}
|
|
}
|
|
}
|
|
// We haven't found a shorter path to this coordinate, so carry on
|
|
if !tPathContains {
|
|
f.testedPath.Append(wrkCoord)
|
|
if f.debug {
|
|
fmt.Println(aoc.ClearScreen, f.start.Name(), "=>", f.end.Name(), "\n", len(f.testedPath.coords), "/", f.cellCount, "\n", "Shortest:", shortestFound)
|
|
f.PrintPath(f.testedPath)
|
|
time.Sleep(time.Millisecond * 50)
|
|
}
|
|
}
|
|
var solvePaths []Path
|
|
var didN, didE, didS, didW bool
|
|
|
|
var preferNS bool
|
|
if math.Abs(float64(f.end.y-y)) > math.Abs(float64(f.end.x-x)) {
|
|
// Favor N/S movements
|
|
preferNS = true
|
|
}
|
|
|
|
for tdir := 0; tdir < 2; tdir++ {
|
|
if preferNS {
|
|
switch {
|
|
case f.end.y < y && !didN:
|
|
f.TestAndUpdate(x, y-1, dist+1)
|
|
if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didN = true
|
|
case f.end.y > y && !didS:
|
|
f.TestAndUpdate(x, y+1, dist+1)
|
|
if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didS = true
|
|
case f.end.x > x && !didE:
|
|
f.TestAndUpdate(x+1, y, dist+1)
|
|
if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didE = true
|
|
case f.end.x < x && !didW:
|
|
f.TestAndUpdate(x-1, y, dist+1)
|
|
if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didW = true
|
|
}
|
|
} else {
|
|
switch {
|
|
case f.end.x > x && !didE:
|
|
f.TestAndUpdate(x+1, y, dist+1)
|
|
if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didE = true
|
|
case f.end.x < x && !didW:
|
|
f.TestAndUpdate(x-1, y, dist+1)
|
|
if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didW = true
|
|
case f.end.y < y && !didN:
|
|
f.TestAndUpdate(x, y-1, dist+1)
|
|
if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didN = true
|
|
case f.end.y > y && !didS:
|
|
f.TestAndUpdate(x, y+1, dist+1)
|
|
if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
didS = true
|
|
}
|
|
}
|
|
}
|
|
if !didN {
|
|
f.TestAndUpdate(x, y-1, dist+1)
|
|
if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
}
|
|
if !didE {
|
|
f.TestAndUpdate(x+1, y, dist+1)
|
|
if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
}
|
|
if !didS {
|
|
f.TestAndUpdate(x, y+1, dist+1)
|
|
if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
}
|
|
if !didW {
|
|
f.TestAndUpdate(x-1, y, dist+1)
|
|
if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
|
|
p.Append(wrkCoord)
|
|
solvePaths = append(solvePaths, p)
|
|
}
|
|
}
|
|
|
|
var sol bool
|
|
if sol = len(solvePaths) > 0; sol {
|
|
shortest = &solvePaths[0]
|
|
for i := range solvePaths {
|
|
if solvePaths[i].Length()+1 < shortest.Length() {
|
|
shortest = &solvePaths[i]
|
|
}
|
|
}
|
|
}
|
|
return *shortest, sol
|
|
}
|
|
|
|
func (f *Floor) TestAndUpdate(x, y, dist int) {
|
|
if t := f.testedPath.GetCoordAt(x, y); t != nil {
|
|
if t.dist > dist+1 {
|
|
f.testedPath.SetCoordDist(x, y, dist+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
type Coord struct {
|
|
x, y, dist int
|
|
label byte
|
|
}
|
|
|
|
func NewCoord(x, y int, label byte) *Coord {
|
|
return &Coord{x, y, -1, label}
|
|
}
|
|
|
|
func (c *Coord) Name() string {
|
|
if c.label != '#' && c.label != '.' && c.label != 0 {
|
|
return string(c.label)
|
|
}
|
|
return fmt.Sprint("(", c.x, ",", c.y, ")")
|
|
}
|
|
|
|
func (c *Coord) Is(x, y int) bool {
|
|
return c.x == x && c.y == y
|
|
}
|
|
|
|
type CoordByLabel []Coord
|
|
|
|
func (a CoordByLabel) Len() int { return len(a) }
|
|
func (a CoordByLabel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a CoordByLabel) Less(i, j int) bool { return a[i].label < a[j].label }
|
|
|
|
type Path struct {
|
|
coords []Coord
|
|
}
|
|
|
|
func (p *Path) Append(c Coord) {
|
|
p.coords = append(p.coords, c)
|
|
}
|
|
|
|
func (p *Path) ContainsCoord(x, y int) bool {
|
|
for i := range p.coords {
|
|
if p.coords[i].Is(x, y) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Path) GetCoordAt(x, y int) *Coord {
|
|
for i := range p.coords {
|
|
if p.coords[i].Is(x, y) {
|
|
return &p.coords[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Path) SetCoordDist(x, y, dist int) bool {
|
|
for i := range p.coords {
|
|
if p.coords[i].Is(x, y) {
|
|
p.coords[i].dist = dist
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Path) Length() int {
|
|
return len(p.coords)
|
|
}
|
|
|
|
func (p *Path) Name() string {
|
|
return p.coords[0].Name() + " -> " + p.coords[len(p.coords)-1].Name()
|
|
}
|