diff --git a/2017/day07/main.go b/2017/day07/main.go new file mode 100644 index 0000000..f83cd76 --- /dev/null +++ b/2017/day07/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathutil" + "github.com/alexchao26/advent-of-code-go/util" +) + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(util.ReadFile("./input.txt")) + fmt.Println("Output:", ans) + } else { + ans := part2(util.ReadFile("./input.txt")) + fmt.Println("Output:", ans) + } +} + +func part1(input string) string { + graph := parseInput(input) + + allNames := map[string]bool{} + for k := range graph { + allNames[k] = true + } + + // iterate through all graph edges and remove any dependants from allNames + // map b/c that cannot be at the bottom of the stack + for _, node := range graph { + for _, name := range node.edges { + delete(allNames, name) + } + } + + if len(allNames) != 1 { + panic("Expected one name left, got" + mathutil.IntToStr(len(allNames))) + } + + // have to iterate over graph to get remaining name + var nameAtBottom string + for name := range allNames { + nameAtBottom = name + } + return nameAtBottom +} + +// NOTE this is not a fully generalized solution, there are graphs where this +// could fail. Although I suspect AoC inputs are tailored to make this solution +// sufficient +func part2(input string) int { + graph := parseInput(input) + currentNode := part1(input) + weightCalculator := memoCalcWeight(graph) + + var siblings []string + // should not run for more than the number of nodes in the graph + for indexToExit := 0; indexToExit < len(graph); indexToExit++ { + // store dependents which have a particular weight + weightToDependents := map[int][]string{} + for _, dependentName := range graph[currentNode].edges { + weight := weightCalculator(dependentName) + weightToDependents[weight] = append(weightToDependents[weight], dependentName) + } + + // one of the dependents has a different weight than the others, dive into it + if len(weightToDependents) > 1 { + // store its siblings b/c IF the next loop finds that all dependents + // are an equal weight, we need to compare the current set of siblings + siblings = graph[currentNode].edges + + // find the node to dive into + for _, names := range weightToDependents { + if len(names) == 1 { + currentNode = names[0] + } + } + } else if len(weightToDependents) == 1 { + // if dependents all have the same weight, this node is the problem. + // compare to its siblings to determine what its weight should be + currentWeight := weightCalculator(currentNode) + for _, sib := range siblings { + if sib != currentNode { + desiredWeight := weightCalculator(sib) + // apply diff to current node's weight and return it + return graph[currentNode].weight - (currentWeight - desiredWeight) + } + } + } else { + panic("unhandled case, weightToDependents == 0") + } + } + + panic("something's wrong in the loop...") +} + +// memoized: calculate the weight, including children, of a given node name +func memoCalcWeight(graph map[string]graphNode) func(string) int { + memo := make(map[string]int, len(graph)) + + var closureFunc func(string) int + closureFunc = func(rootName string) int { + // return from memo if possible + if wt, ok := memo[rootName]; ok { + return wt + } + + // otherwise calculate & set in memo + sum := graph[rootName].weight + for _, dependent := range graph[rootName].edges { + sum += closureFunc(dependent) + } + memo[rootName] = sum + + return sum + } + return closureFunc +} + +type graphNode struct { + name string // unused but useful for debugging + weight int + edges []string +} + +func parseInput(input string) map[string]graphNode { + lines := strings.Split(input, "\n") + graph := make(map[string]graphNode, len(lines)) + for _, l := range lines { + parts := strings.Split(l, " -> ") + + leftParts := strings.Split(parts[0], " ") + name := leftParts[0] + weight := mathutil.StrToInt(leftParts[1][1 : len(leftParts[1])-1]) + + var edges []string + if len(parts) == 2 { + edges = strings.Split(parts[1], ", ") + // assumption confirmed that all nodes have zero or >= 2 edges + if len(edges) == 1 { + panic("Assumed no nodes have exactly one dependant, but got 1 for node: " + name) + } + } + graph[name] = graphNode{ + name: name, + weight: weight, + edges: edges, + } + } + + return graph +} diff --git a/2017/day07/main_test.go b/2017/day07/main_test.go new file mode 100644 index 0000000..1c6799e --- /dev/null +++ b/2017/day07/main_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "github.com/alexchao26/advent-of-code-go/util" +) + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"actual", util.ReadFile("input.txt"), "xegshds"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + {"actual", util.ReadFile("input.txt"), 299}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}