From 9177d35cafbf64bf04aa3af27be9571a168abc12 Mon Sep 17 00:00:00 2001 From: efim Date: Mon, 25 Dec 2023 07:53:17 +0000 Subject: [PATCH] day25: initial graph stuff, with cycles removal not working --- .gitignore | 1 + day25/Snowerload.go | 10 ++ day25/example | 13 ++ day25/example-after-removing.mmd | 29 ++++ day25/example-before-removing.mmd | 34 ++++ day25/example-graph.mmd | 34 ++++ day25/example-graph.mmd.svg | 1 + day25/example2 | 3 + day25/graph.go | 254 ++++++++++++++++++++++++++++++ day25/graph_test.go | 64 ++++++++ day25/notes.org | 30 ++++ main.go | 6 +- 12 files changed, 476 insertions(+), 3 deletions(-) create mode 100644 day25/Snowerload.go create mode 100644 day25/example create mode 100644 day25/example-after-removing.mmd create mode 100644 day25/example-before-removing.mmd create mode 100644 day25/example-graph.mmd create mode 100644 day25/example-graph.mmd.svg create mode 100644 day25/example2 create mode 100644 day25/graph.go create mode 100644 day25/graph_test.go create mode 100644 day25/notes.org diff --git a/.gitignore b/.gitignore index e22d7e5..e5ea7bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.direnv/ /.go input +*.png diff --git a/day25/Snowerload.go b/day25/Snowerload.go new file mode 100644 index 0000000..05a4e0a --- /dev/null +++ b/day25/Snowerload.go @@ -0,0 +1,10 @@ +package day25 + +import ( + "fmt" +) + +func Run() int { + fmt.Println("time to wrap things up") + return 0 +} diff --git a/day25/example b/day25/example new file mode 100644 index 0000000..bbfda0b --- /dev/null +++ b/day25/example @@ -0,0 +1,13 @@ +jqt: rhn xhk nvd +rsh: frs pzl lsr +xhk: hfx +cmg: qnr nvd lhk bvb +rhn: xhk bvb hfx +bvb: xhk hfx +pzl: lsr hfx nvd +qnr: nvd +ntq: jqt hfx bvb xhk +nvd: lhk +lsr: lhk +rzs: qnr cmg lsr rsh +frs: qnr lhk lsr diff --git a/day25/example-after-removing.mmd b/day25/example-after-removing.mmd new file mode 100644 index 0000000..fd64116 --- /dev/null +++ b/day25/example-after-removing.mmd @@ -0,0 +1,29 @@ +flowchart TD + hfx --- xhk + frs --- qnr + qnr --- rzs + rsh --- rzs + bvb --- rhn + jqt --- ntq + lsr --- rsh + jqt --- xhk + frs --- rsh + bvb --- xhk + frs --- lsr + hfx --- pzl + cmg --- rzs + cmg --- lhk + jqt --- nvd + bvb --- cmg + hfx --- ntq + nvd --- pzl + rhn --- xhk + lsr --- rzs + cmg --- qnr + cmg --- nvd + bvb --- ntq + lhk --- lsr + hfx --- rhn + pzl --- rsh + lsr --- pzl + frs --- lhk diff --git a/day25/example-before-removing.mmd b/day25/example-before-removing.mmd new file mode 100644 index 0000000..9d75a39 --- /dev/null +++ b/day25/example-before-removing.mmd @@ -0,0 +1,34 @@ +flowchart TD + lhk --- lsr + cmg --- lhk + frs --- lhk + frs --- qnr + jqt --- xhk + lsr --- rsh + jqt --- ntq + bvb --- xhk + hfx --- rhn + jqt --- rhn + lsr --- pzl + nvd --- pzl + ntq --- xhk + bvb --- ntq + cmg --- qnr + rhn --- xhk + rsh --- rzs + frs --- lsr + frs --- rsh + hfx --- xhk + hfx --- pzl + cmg --- nvd + lhk --- nvd + nvd --- qnr + jqt --- nvd + lsr --- rzs + qnr --- rzs + cmg --- rzs + hfx --- ntq + bvb --- hfx + bvb --- cmg + bvb --- rhn + pzl --- rsh diff --git a/day25/example-graph.mmd b/day25/example-graph.mmd new file mode 100644 index 0000000..4de1636 --- /dev/null +++ b/day25/example-graph.mmd @@ -0,0 +1,34 @@ +flowchart TD + hfx --- pzl + cmg --- lhk + lsr --- rsh + rsh --- rzs + bvb --- rhn + jqt --- xhk + nvd --- pzl + lsr --- pzl + frs --- qnr + frs --- lsr + lhk --- lsr + lsr --- rzs + rhn --- xhk + hfx --- ntq + nvd --- qnr + qnr --- rzs + bvb --- xhk + hfx --- xhk + jqt --- rhn + jqt --- nvd + cmg --- nvd + lhk --- nvd + frs --- rsh + ntq --- xhk + cmg --- rzs + bvb --- ntq + cmg --- qnr + hfx --- rhn + jqt --- ntq + bvb --- cmg + frs --- lhk + pzl --- rsh + bvb --- hfx diff --git a/day25/example-graph.mmd.svg b/day25/example-graph.mmd.svg new file mode 100644 index 0000000..c6578d8 --- /dev/null +++ b/day25/example-graph.mmd.svg @@ -0,0 +1 @@ +
hfx
pzl
cmg
lhk
lsr
rsh
rzs
bvb
rhn
jqt
xhk
nvd
frs
qnr
ntq
\ No newline at end of file diff --git a/day25/example2 b/day25/example2 new file mode 100644 index 0000000..ee867b4 --- /dev/null +++ b/day25/example2 @@ -0,0 +1,3 @@ +jqt: rhn nvd rsh +rsh: frs pzl lsr +xhk: hfx diff --git a/day25/graph.go b/day25/graph.go new file mode 100644 index 0000000..fb9c94b --- /dev/null +++ b/day25/graph.go @@ -0,0 +1,254 @@ +package day25 + +import ( + "fmt" + "log" + "os" + "strings" + + mapset "github.com/deckarep/golang-set/v2" +) + +type Graph struct { + Nodes map[string]*Node +} + +type Node struct { + Name string + Neighbors mapset.Set[string] +} + +func (n Node) String() string { + return fmt.Sprintf("[%s : %+v]", n.Name, n.Neighbors) +} + +func ReadGraphFile(filename string) (g Graph) { + g.Nodes = map[string]*Node{} + + bytes, err := os.ReadFile(filename) + if err != nil { + panic(err) + } + text := strings.TrimSpace(string(bytes)) + for _, line := range strings.Split(text, "\n") { + g.readGraphLine(line) + } + + return +} + +func (g *Graph) readGraphLine(l string) (n Node) { + firstSplit := strings.Split(l, ":") + n.Name = firstSplit[0] + n.Neighbors = mapset.NewSet[string]() + + secondSplit := strings.Fields(firstSplit[1]) + + for _, neighborName := range secondSplit { + neighbor, exists := g.Nodes[neighborName] + if !exists { + neighbor = &Node{ + Name: neighborName, + Neighbors: mapset.NewSet[string](), + } + g.Nodes[neighborName] = neighbor + } + neighbor.Neighbors.Add(n.Name) + n.Neighbors.Add(neighbor.Name) + } + + g.Nodes[n.Name] = &n + + return +} + +func (g *Graph) RemoveEdge(a, b string) { + // log.Printf("entering remove edge for %s and %s", a, b) + nodeA, existsA := g.Nodes[a] + // log.Println("got first node", nodeA, existsA) + nodeB, existsB := g.Nodes[b] + // log.Println("got second node", nodeB, existsB) + if !existsA || !existsB { + panic("requesting not found node") + } + + // log.Println("before removals") + newANeighbors := mapset.NewSet[string]() + for oldNeighbor := range nodeA.Neighbors.Iter() { + if oldNeighbor != nodeB.Name { + newANeighbors.Add(oldNeighbor) + } + } + nodeA.Neighbors = newANeighbors + // nodeA.Neighbors.Remove(nodeB.Name) + + // log.Println("removed first", nodeA) + newBNeighbors := mapset.NewSet[string]() + for oldNeighbor := range nodeB.Neighbors.Iter() { + if oldNeighbor != nodeB.Name { + newBNeighbors.Add(oldNeighbor) + } + } + nodeB.Neighbors = newBNeighbors + // nodeB.Neighbors.Remove(nodeA.Name) + // log.Println("removed second", nodeB) +} + +func (g *Graph) findCycle() (from, to string, exists bool) { + log.Printf(">>>> starting new find cycle") + var firstNode *Node + for _, n := range g.Nodes { + firstNode = n + break + } + initialVisited := mapset.NewSet[string](firstNode.Name) + for neighborName := range firstNode.Neighbors.Iter() { + from, to, exists = g.dfcCycle(firstNode.Name, neighborName, initialVisited) + if exists { + break + } + } + + log.Printf("<<<< cycle %t, from %s to %s", exists, from, to) + return +} + +func (g *Graph) dfcCycle(fromName, atName string, visited mapset.Set[string]) (cycleFrom, cycleTo string, cycleExists bool) { + // log.Printf("> step from %+v to %+v. visited : %+v", fromName, atName, visited) + if visited.Cardinality() == len(g.Nodes) { + return + } + + atNode := g.Nodes[atName] + + if visited.Contains(atName) { + return fromName, atName, true + } + + newVisited := visited.Clone() + newVisited.Add(atName) + for neighborName := range atNode.Neighbors.Iter() { + cycleFrom, cycleTo, cycleExists = g.dfcCycle(atName, neighborName, newVisited) + if cycleExists { + break + } + } + + return +} + +func (g *Graph) DoThreeRemovals() { + from, to, found := g.findCycle() + log.Printf("first result %s, %s, %t", from, to, found) + // graph_test.go:26: first result xhk, rhn, true + g.RemoveEdge("xhk", "rhn") + + from, to, found = g.findCycle() + log.Printf("second result %s, %s, %t", from, to, found) + // graph_test.go:32: second result qnr, nvd, true + g.RemoveEdge("qnr", "nvd") + + from, to, found = g.findCycle() + log.Printf("third result %s, %s, %t", from, to, found) + // graph_test.go:36: third result lhk, cmg, true + + g.RemoveEdge("lhk", "cmg") + from, to, found = g.findCycle() + log.Printf("fourth result %s, %s, %t", from, to, found) + + // OK, so there could be more than 3 cycles. + // but if i want to break things into 2 components, i need to do more investigating + return +} + +func (g *Graph) ComponentFrom(fromName string) (component mapset.Set[string]) { + startNode := g.Nodes[fromName] + component = mapset.NewSet[string](startNode.Name) + toVisit := startNode.Neighbors.Clone() + + for toVisit.Cardinality() > 0 { + runnerNodeName, _ := toVisit.Pop() + if component.Contains(runnerNodeName) { + continue + } + component.Add(runnerNodeName) + runnerNode := g.Nodes[runnerNodeName] + unvisitedNeighbors := runnerNode.Neighbors.Difference(component) + // log.Printf("adding %s to component. neighbors %+v, adding %+v to visit", + // runnerNodeName, runnerNode.Neighbors, unvisitedNeighbors) + toVisit = toVisit.Union(unvisitedNeighbors) + } + + return +} + +func (g *Graph) ToMermaid() (result string) { + result += "flowchart TD\n" + edges := mapset.NewSet[string]() + + for _, node := range g.Nodes { + for neighborName := range node.Neighbors.Iter() { + var first, second string + if node.Name < neighborName { + first = node.Name + second = neighborName + } else { + first = neighborName + second = node.Name + } + edges.Add(fmt.Sprintf("\t%s --- %s\n", first, second)) + } + } + + for line := range edges.Iter() { + result += line + } + return +} + +func (g *Graph)SaveAsMermaid(filename string) { + mmd := g.ToMermaid() + + file, err := os.Create(filename) + if err != nil { + panic(err) + } + defer func() { + if err := file.Close(); err != nil { + panic(err) + } + }() + + file.WriteString(mmd) +} + +type Edge struct { + smaller, bigger string +} + +func CreateEdge(a, b string) Edge { + var smaller, bigger string + if a < b { + smaller = a + bigger = b + } else { + smaller = b + bigger = a + } + return Edge{smaller, bigger} +} + +func (g *Graph) RemoveAllCycles() (removedEdges mapset.Set[Edge]) { + removedEdges = mapset.NewSet[Edge]() + hasCycle := true + var from, to string + for hasCycle { + from, to, hasCycle = g.findCycle() + if hasCycle { + edgeToRemove := CreateEdge(from, to) + removedEdges.Add(edgeToRemove) + g.RemoveEdge(from, to) + } + } + return +} diff --git a/day25/graph_test.go b/day25/graph_test.go new file mode 100644 index 0000000..d76384a --- /dev/null +++ b/day25/graph_test.go @@ -0,0 +1,64 @@ +package day25 + +import ( + "testing" +) + +func TestReadFileExample(t *testing.T) { + filename := "example" + g := ReadGraphFile(filename) + t.Logf("read graph %+v", g) +} + +func TestRemoveEdge(t *testing.T) { + filename := "example" + g := ReadGraphFile(filename) + t.Logf("read graph %+v", g) + + g.RemoveEdge("bvb", "hfx") + t.Logf("after removing bvb-hfv %+v", g) +} + +func TestRunFindCycle(t *testing.T) { + filename := "example" + g := ReadGraphFile(filename) + g.DoThreeRemovals() +} + +func TestCreateExampleMermaid(t *testing.T) { + filename := "example" + g := ReadGraphFile(filename) + + g.SaveAsMermaid("example-graph.mmd") +} + +func TestComponentOnInitial(t *testing.T) { + // should be all nodes + filename := "example" + g := ReadGraphFile(filename) + comp := g.ComponentFrom("bvb") + t.Logf("got component %+v", comp) + if comp.Cardinality() != len(g.Nodes) { + t.Errorf("should have same size!") + } +} + +func TestComponentOnMini(t *testing.T) { + // should be all nodes + filename := "example2" + g := ReadGraphFile(filename) + comp := g.ComponentFrom("jqt") + t.Logf("got component %+v", comp) + if comp.Cardinality() == len(g.Nodes) { + t.Errorf("should have different size!") + } +} + +func TestRemoveAllCycles(t *testing.T) { + filename := "example" + g := ReadGraphFile(filename) + g.SaveAsMermaid("example-before-removing.mmd") + edges := g.RemoveAllCycles() + t.Logf("removed edges %+v", edges) + g.SaveAsMermaid("example-after-removing.mmd") +} diff --git a/day25/notes.org b/day25/notes.org new file mode 100644 index 0000000..bb0fa31 --- /dev/null +++ b/day25/notes.org @@ -0,0 +1,30 @@ +#+title: Notes +* ok, not so simple +'how to find 3 edges which if removed split graph into 2 components' + +i guess i could find all cycles? +and then what? + +then if i have graph without cycles and all of the removed edges, +maybe i can figure out how to remove one edge, to make two components, and all (k - 2) edges back in a way that would not connect those components. + +well, the cycles maybe should have been broken elsewhere? +for most cycles - it doesn't matter. + +for cycles that break into 2 components, does it? + +ok. if i have the no-cycle graph. +then we can maybe start searching for 'third' edge to remove. + +i remove it, i get two components. +then as a test - exactly two edges when added should connect those components. +if that happens - i have an answer + +sounds good +** making code to get components. +now. +** make method to remove all cycles. +save the removed edges to slice + +i guess could be a method that modifies the graph and returns all removed edges. +and test that diff --git a/main.go b/main.go index 5fda45c..0356ac7 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,15 @@ import ( "log" "time" - "sunshine.industries/aoc2023/day24" + "sunshine.industries/aoc2023/day25" ) func main() { startTime := time.Now() log.Print("> starting run:") - result := day24.Run() - log.Printf("\n\nday24 result: %d\n****\n", result) + result := day25.Run() + log.Printf("\n\nday25 result: %d\n****\n", result) endTime := time.Now() diff := endTime.Sub(startTime) log.Printf("execution took %s", diff.String())