diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b47e8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +day03_debug.png diff --git a/day03.go b/day03.go new file mode 100644 index 0000000..48deae2 --- /dev/null +++ b/day03.go @@ -0,0 +1,269 @@ +package aoc2019 + +import ( + "image" + "image/color" + "io/ioutil" + "math" + "strconv" + "strings" + + "github.com/llgcode/draw2d/draw2dimg" + "github.com/pkg/errors" +) + +type day3LineSegment [4]int + +func (d day3LineSegment) Draw(img *image.RGBA, col color.Color) { + var ptMod func(x, y int) (int, int, bool) + + switch { + case d.IsVertical() && d[1] < d[3]: + ptMod = func(x, y int) (int, int, bool) { return x, y + 1, y+1 <= d[3] } + case d.IsVertical() && d[1] > d[3]: + ptMod = func(x, y int) (int, int, bool) { return x, y - 1, y-1 >= d[3] } + case !d.IsVertical() && d[0] < d[2]: + ptMod = func(x, y int) (int, int, bool) { return x + 1, y, x+1 <= d[2] } + case !d.IsVertical() && d[0] > d[2]: + ptMod = func(x, y int) (int, int, bool) { return x - 1, y, x-1 >= d[2] } + } + + var pX, pY, ok = d[0], d[1], true + + for ok { + img.Set(pX, pY, col) + pX, pY, ok = ptMod(pX, pY) + } +} + +func (d day3LineSegment) GetIntersection(in day3LineSegment) ([2]int, bool) { + if d.IsVertical() == in.IsVertical() { + // Both lines have the same direction, there is no intersection + // NOTE: This might yield false negative when lines are overlapping? + return [2]int{0, 0}, false + } + + var ( + vert, horiz day3LineSegment + ) + + if d.IsVertical() { + vert, horiz = d, in + } else { + vert, horiz = in, d + } + + // Normalize lines for intersection finding + if vert[1] > vert[3] { + vert[1], vert[3] = vert[3], vert[1] + } + + if horiz[0] > horiz[2] { + horiz[0], horiz[2] = horiz[2], horiz[0] + } + + if vert[0] < horiz[0] || vert[0] > horiz[2] || horiz[1] < vert[1] || horiz[1] > vert[3] { + // Lines do not have an intersection + return [2]int{0, 0}, false + } + + return [2]int{vert[0], horiz[1]}, true +} + +func (d day3LineSegment) HasPoint(x, y int) bool { + switch { + case d.IsVertical() && x != d[0]: // vertical line, test Y not on line + fallthrough + case !d.IsVertical() && y != d[1]: // horizontal line, text X not on line + fallthrough + case d.IsVertical() && d[1] < d[3] && (y < d[1] || y > d[3]): // vertical low to high, test Y not in range + fallthrough + case d.IsVertical() && d[1] > d[3] && (y > d[1] || y < d[3]): // vertical high to low, test Y not in range + fallthrough + case !d.IsVertical() && d[0] < d[2] && (x < d[0] || x > d[2]): // horizontal left to right, test X not in range + fallthrough + case !d.IsVertical() && d[0] > d[2] && (x > d[0] || x < d[2]): // horizontal right to left, test X not in range + + return false + } + + return true +} + +func (d day3LineSegment) IsVertical() bool { + return d[0] == d[2] +} + +func (d day3LineSegment) Steps() int { + if d.IsVertical() { + return int(math.Abs(float64(d[1] - d[3]))) + } + return int(math.Abs(float64(d[0] - d[2]))) +} + +func (d day3LineSegment) StepsToPoint(x, y int) int { + if d.IsVertical() { + return int(math.Abs(float64(d[1] - y))) + } + return int(math.Abs(float64(d[0] - x))) +} + +type day3Line []day3LineSegment + +func (d day3Line) GetIntersections(in day3Line) [][2]int { + var inter [][2]int + + for _, dseg := range d { + for _, iseg := range in { + if is, ok := dseg.GetIntersection(iseg); ok { + inter = append(inter, is) + } + } + } + + return inter +} + +func debugDay3DrawImage(l1, l2 day3Line) error { + dest := image.NewRGBA(image.Rect(-500, -500, 500, 500)) + + // Draw l1 in red + for _, ls := range l1 { + ls.Draw(dest, color.RGBA{0xff, 0x0, 0x0, 0xff}) + } + + // Draw l2 in blue + for _, ls := range l2 { + ls.Draw(dest, color.RGBA{0x0, 0x0, 0xff, 0xff}) + } + + // Draw detected intersections in purple + for _, i := range l1.GetIntersections(l2) { + dest.SetRGBA(i[0], i[1], color.RGBA{0xff, 0x0, 0xff, 0xff}) + } + + // Draw "home-point" in green + dest.SetRGBA(0, 0, color.RGBA{0x0, 0xff, 0x0, 0xff}) + + draw2dimg.SaveToPngFile("day03_debug.png", dest) + + return nil +} + +func getDay3MinIntersectionDistance(l1, l2 day3Line, originX, originY int) int { + var ( + inter = l1.GetIntersections(l2) + min = math.MaxInt64 + ) + + for _, i := range inter { + dist := manhattenDistance(originX, originY, i[0], i[1]) + if dist > 0 && dist < min { + min = dist + } + } + + return min +} + +func getDay3MinIntersectionSteps(l1, l2 day3Line) int { + var minSteps int = math.MaxInt64 + + for _, is := range l1.GetIntersections(l2) { + var combinedsteps int + + for _, l := range []day3Line{l1, l2} { + for _, ls := range l { + if ls.HasPoint(is[0], is[1]) { + combinedsteps += ls.StepsToPoint(is[0], is[1]) + break + } + combinedsteps += ls.Steps() + } + } + + if combinedsteps > 0 && combinedsteps < minSteps { + minSteps = combinedsteps + } + } + + return minSteps +} + +func parseDay3LineDefinition(definition string, startX, startY int) (day3Line, error) { + var ( + directions = strings.Split(strings.TrimSpace(definition), ",") + pX, pY = startX, startY + out day3Line + ) + + for _, d := range directions { + l, err := strconv.Atoi(d[1:]) + if err != nil { + return nil, errors.Wrapf(err, "Unable to parse direction %q", d) + } + + var tX, tY int + switch d[0] { + case 'D': + tX, tY = pX, pY-l + + case 'L': + tX, tY = pX-l, pY + + case 'R': + tX, tY = pX+l, pY + + case 'U': + tX, tY = pX, pY+l + + default: + return nil, errors.Wrapf(err, "Invalid direction %q given", d) + } + + out = append(out, [4]int{pX, pY, tX, tY}) + pX, pY = tX, tY + } + + return out, nil +} + +func solveDay3Part1(inFile string) (int, error) { + raw, err := ioutil.ReadFile(inFile) + if err != nil { + return 0, errors.Wrap(err, "Unable to read input") + } + + defs := strings.Split(string(raw), "\n") + l1, err := parseDay3LineDefinition(defs[0], 0, 0) + if err != nil { + return 0, errors.Wrap(err, "Unable to parse L1") + } + + l2, err := parseDay3LineDefinition(defs[1], 0, 0) + if err != nil { + return 0, errors.Wrap(err, "Unable to parse L1") + } + + return getDay3MinIntersectionDistance(l1, l2, 0, 0), nil +} + +func solveDay3Part2(inFile string) (int, error) { + raw, err := ioutil.ReadFile(inFile) + if err != nil { + return 0, errors.Wrap(err, "Unable to read input") + } + + defs := strings.Split(string(raw), "\n") + l1, err := parseDay3LineDefinition(defs[0], 0, 0) + if err != nil { + return 0, errors.Wrap(err, "Unable to parse L1") + } + + l2, err := parseDay3LineDefinition(defs[1], 0, 0) + if err != nil { + return 0, errors.Wrap(err, "Unable to parse L1") + } + + return getDay3MinIntersectionSteps(l1, l2), nil +} diff --git a/day03_input.txt b/day03_input.txt new file mode 100644 index 0000000..bf208e9 --- /dev/null +++ b/day03_input.txt @@ -0,0 +1,2 @@ +R1003,U741,L919,U341,L204,U723,L113,D340,L810,D238,R750,U409,L104,U65,R119,U58,R94,D738,L543,U702,R612,D998,L580,U887,R664,D988,R232,D575,R462,U130,L386,U386,L217,U155,L68,U798,R792,U149,L573,D448,R76,U896,L745,D640,L783,D19,R567,D271,R618,U677,L449,D651,L843,D117,L636,U329,R484,U853,L523,U815,L765,U834,L500,U321,R874,U90,R473,U31,R846,U549,L70,U848,R677,D557,L702,U90,R78,U234,R282,D289,L952,D514,R308,U255,R752,D338,L134,D335,L207,U167,R746,U328,L65,D579,R894,U716,R510,D932,L396,U766,L981,D115,L668,U197,R773,U898,L22,U294,L548,D634,L31,U626,R596,U442,L103,U448,R826,U511,R732,U680,L279,D693,R292,U641,R253,U977,R699,U861,R534,D482,L481,U929,L244,U863,L951,D744,R775,U198,L658,U700,L740,U725,R286,D105,L629,D117,L991,D778,L627,D389,R942,D17,L791,D515,R231,U418,L497,D421,L508,U91,R841,D823,L88,U265,L223,D393,L399,D390,L431,D553,R40,U724,L566,U121,L436,U797,L42,U13,R19,D858,R912,D571,L207,D5,L981,D996,R814,D918,L16,U872,L5,U281,R706,U596,R827,D19,R976,D664,L930,U56,R168,D892,R661,D751,R219,U343,R120,U21,L659,U976,R498,U282,R1,U721,R475,D798,L5,U396,R268,D454,R118,U260,L709,D369,R96,D232,L320,D763,R548,U670,R102,D253,L947,U845,R888,D645,L734,D734,L459,D638,L82,U933,L485,U235,R181,D51,L45,D979,L74,D186,L513,U974,R283,D493,R128,U909,L96,D861,L291,U640,R793,D712,R421,D315,L152,U220,L252,U642,R126,D417,R137,D73,R1,D711,R880,U718,R104,U444,L36,D974,L360,U12,L890,D337,R184,D745,R164,D931,R915,D999,R452,U221,L399,D761,L987,U562,R25,D642,R411,D605,R964 +L1010,U302,L697,D105,R618,U591,R185,U931,R595,D881,L50,D744,L320,D342,L221,D201,L862,D959,R553,D135,L238,U719,L418,U798,R861,U80,L571,U774,L896,U772,L960,U368,R415,D560,R276,U33,L532,U957,R621,D137,R373,U53,L842,U118,L299,U203,L352,D531,R118,U816,R355,U678,L983,D175,R652,U230,R190,D402,R111,D842,R756,D961,L82,U206,L576,U910,R622,D494,R630,D893,L200,U943,L696,D573,L143,D640,L885,D184,L52,D96,L580,U204,L793,D806,R477,D651,L348,D318,L924,D700,R675,D689,L723,D418,L156,D215,L943,D397,L301,U350,R922,D721,R14,U399,L774,U326,L14,D465,L65,U697,R564,D4,L40,D250,R914,U901,R316,U366,R877,D222,L672,D329,L560,U882,R321,D169,R161,U891,L552,U86,L194,D274,L567,D669,L682,U60,L985,U401,R587,U569,L1,D325,L73,U814,L338,U618,L49,U67,L258,D596,R493,D249,L310,D603,R810,D735,L829,D378,R65,U85,L765,D854,L863,U989,L595,U564,L373,U76,R923,U760,L965,U458,L610,U461,R900,U151,L650,D437,L1,U464,L65,D349,R256,D376,L686,U183,L403,D354,R867,U993,R819,D333,L249,U466,L39,D878,R855,U166,L254,D532,L909,U48,L980,U652,R393,D291,L502,U230,L738,U681,L393,U935,L333,D139,L499,D813,R302,D415,L693,D404,L308,D603,R968,U753,L510,D356,L356,U620,R386,D205,R587,U212,R715,U360,L603,U792,R58,U619,R73,D958,L53,D666,L756,U71,L621,D576,L174,U779,L382,U977,R890,D830,R822,U312,R716,U767,R36,U340,R322,D175,L417,U710,L313,D526,L573,D90,L493,D257,L918,U425,R93,D552,L691,U792,R189,U43,L633,U934,L953,U817,L404,D904,L384,D15,L670,D889,L648,U751,L928,D744,L932,U761,R879,D229,R491,U902,R134,D219,L634,U423,L241 diff --git a/day03_test.go b/day03_test.go new file mode 100644 index 0000000..2162b89 --- /dev/null +++ b/day03_test.go @@ -0,0 +1,102 @@ +package aoc2019 + +import ( + "reflect" + "strings" + "testing" +) + +func TestParseDay3LineDefinition(t *testing.T) { + for ld, expLine := range map[string]day3Line{ + "R5": {{0, 0, 5, 0}}, + "R8,U5,L5,D3": {{0, 0, 8, 0}, {8, 0, 8, 5}, {8, 5, 3, 5}, {3, 5, 3, 2}}, + "U7,R6,D4,L4": {{0, 0, 0, 7}, {0, 7, 6, 7}, {6, 7, 6, 3}, {6, 3, 2, 3}}, + } { + l, err := parseDay3LineDefinition(ld, 0, 0) + if err != nil { + t.Fatalf("Day 3 line parser failed: %s", err) + } + + if !reflect.DeepEqual(expLine, l) { + t.Errorf("Mismatch in line of definition %q: exp=%+v got=%+v", ld, expLine, l) + } + } +} + +func TestDay3LineGetIntersections(t *testing.T) { + for ld, expInter := range map[string][][2]int{ + "R8,U5,L5,D3|U7,R6,D4,L4": {{0, 0}, {6, 5}, {3, 3}}, + "R75,D30,R83,U83,L12,D49,R71,U7,L72|U62,R66,U55,R34,D71,R55,D58,R83": {{0, 0}, {158, -12}, {146, 46}, {155, 4}, {155, 11}}, + } { + lds := strings.Split(ld, "|") + l1, _ := parseDay3LineDefinition(lds[0], 0, 0) + l2, _ := parseDay3LineDefinition(lds[1], 0, 0) + + inter := l1.GetIntersections(l2) + if !reflect.DeepEqual(expInter, inter) { + t.Errorf("Mismatch in line inters of def %q: exp=%+v got=%+v", ld, expInter, inter) + } + } +} + +func TestDay3MinIntersectionDistance(t *testing.T) { + for ld, expDist := range map[string]int{ + "R8,U5,L5,D3|U7,R6,D4,L4": 6, + "R75,D30,R83,U83,L12,D49,R71,U7,L72|U62,R66,U55,R34,D71,R55,D58,R83": 159, + "R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51|U98,R91,D20,R16,D67,R40,U7,R15,U6,R7": 135, + } { + lds := strings.Split(ld, "|") + l1, _ := parseDay3LineDefinition(lds[0], 0, 0) + l2, _ := parseDay3LineDefinition(lds[1], 0, 0) + + if dist := getDay3MinIntersectionDistance(l1, l2, 0, 0); dist != expDist { + t.Errorf("Mismatch in intersection distance of def %q: exp=%d got=%d", ld, expDist, dist) + } + } +} + +func TestDay3MinIntersectionSteps(t *testing.T) { + for ld, expDist := range map[string]int{ + "R8,U5,L5,D3|U7,R6,D4,L4": 30, + "R75,D30,R83,U83,L12,D49,R71,U7,L72|U62,R66,U55,R34,D71,R55,D58,R83": 610, + "R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51|U98,R91,D20,R16,D67,R40,U7,R15,U6,R7": 410, + } { + lds := strings.Split(ld, "|") + l1, _ := parseDay3LineDefinition(lds[0], 0, 0) + l2, _ := parseDay3LineDefinition(lds[1], 0, 0) + + if dist := getDay3MinIntersectionSteps(l1, l2); dist != expDist { + t.Errorf("Mismatch in intersection steps of def %q: exp=%d got=%d", ld, expDist, dist) + } + } +} + +func TestDay3DebugRender(t *testing.T) { + var ld = "R75,D30,R83,U83,L12,D49,R71,U7,L72|U62,R66,U55,R34,D71,R55,D58,R83" + + lds := strings.Split(ld, "|") + l1, _ := parseDay3LineDefinition(lds[0], 0, 0) + l2, _ := parseDay3LineDefinition(lds[1], 0, 0) + + if err := debugDay3DrawImage(l1, l2); err != nil { + t.Fatalf("Day 3 debug failed: %s", err) + } +} + +func TestCalculateDay3_Part1(t *testing.T) { + codeP0, err := solveDay3Part1("day03_input.txt") + if err != nil { + t.Fatalf("Day 3 solver failed: %s", err) + } + + t.Logf("Solution Day 3 Part 1: %d", codeP0) +} + +func TestCalculateDay3_Part2(t *testing.T) { + codeP0, err := solveDay3Part2("day03_input.txt") + if err != nil { + t.Fatalf("Day 3 solver failed: %s", err) + } + + t.Logf("Solution Day 3 Part 2: %d", codeP0) +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..addc0b2 --- /dev/null +++ b/helpers.go @@ -0,0 +1,7 @@ +package aoc2019 + +import "math" + +func manhattenDistance(x1, y1, x2, y2 int) int { + return int(math.Abs(float64(x1-x2)) + math.Abs(float64(y1-y2))) +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..fc7954d --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,16 @@ +package aoc2019 + +import "testing" + +func TestManhattenDistance(t *testing.T) { + for expDist, p := range map[int][4]int{ + 2: {0, 0, 0, 2}, + 3: {0, 0, 3, 0}, + 6: {-3, 0, 3, 0}, + 7: {-3, 0, 0, 4}, + } { + if dist := manhattenDistance(p[0], p[1], p[2], p[3]); dist != expDist { + t.Errorf("Unexpected distance for %+v: exp=%d got=%d", p, expDist, dist) + } + } +}