← run

go-03-detect-cycle

0.000
0/1 tests· algorithms
Challenge · difficulty 3/5
# Detect cycle in a directed graph

Implement **`solution.go`** in `package challenge` exporting:

```go
func HasCycle(graph map[string][]string) bool
```

The graph is given as an adjacency map: each key is a node, and its value is the slice of
nodes it has directed edges to. Return `true` if the directed graph contains **any cycle**,
and `false` otherwise.

Rules:

- A **self-loop** (`"a" -> "a"`) counts as a cycle.
- A neighbor that does not appear as a key in the map is a valid node with no outgoing edges.
- An empty map (or `nil`) has no cycle → `false`.
- Duplicate edges are allowed and must not break detection.

Examples:

- `HasCycle(map[string][]string{"a": {"b"}, "b": {"c"}, "c": {}})` → `false` (a DAG)
- `HasCycle(map[string][]string{"a": {"a"}})` → `true` (self-loop)
- `HasCycle(map[string][]string{"a": {"b"}, "b": {"c"}, "c": {"a"}})` → `true` (3-node cycle)
- `HasCycle(map[string][]string{})` → `false`
tests/solution_test.go
package challenge

import "testing"

func TestHasCycle(t *testing.T) {
	cases := []struct {
		name  string
		graph map[string][]string
		want  bool
	}{
		{
			name:  "nil graph",
			graph: nil,
			want:  false,
		},
		{
			name:  "empty graph",
			graph: map[string][]string{},
			want:  false,
		},
		{
			name:  "single node no edges",
			graph: map[string][]string{"a": {}},
			want:  false,
		},
		{
			name:  "simple DAG",
			graph: map[string][]string{"a": {"b"}, "b": {"c"}, "c": {}},
			want:  false,
		},
		{
			name: "diamond DAG (shared descendant, no cycle)",
			graph: map[string][]string{
				"a": {"b", "c"},
				"b": {"d"},
				"c": {"d"},
				"d": {},
			},
			want: false,
		},
		{
			name:  "self-loop",
			graph: map[string][]string{"a": {"a"}},
			want:  true,
		},
		{
			name:  "two node cycle",
			graph: map[string][]string{"a": {"b"}, "b": {"a"}},
			want:  true,
		},
		{
			name:  "three node cycle",
			graph: map[string][]string{"a": {"b"}, "b": {"c"}, "c": {"a"}},
			want:  true,
		},
		{
			name: "cycle reachable only from one component",
			graph: map[string][]string{
				"x": {"y"},
				"y": {},
				"a": {"b"},
				"b": {"c"},
				"c": {"b"},
			},
			want: true,
		},
		{
			name: "neighbor not a key (implicit leaf), no cycle",
			graph: map[string][]string{
				"a": {"b"},
				"b": {"c"}, // c is not a key
			},
			want: false,
		},
		{
			name: "duplicate edges, no cycle",
			graph: map[string][]string{
				"a": {"b", "b", "b"},
				"b": {},
			},
			want: false,
		},
		{
			name: "duplicate edges forming cycle",
			graph: map[string][]string{
				"a": {"b", "b"},
				"b": {"a", "a"},
			},
			want: true,
		},
		{
			name: "long chain no cycle",
			graph: map[string][]string{
				"a": {"b"}, "b": {"c"}, "c": {"d"}, "d": {"e"}, "e": {"f"}, "f": {},
			},
			want: false,
		},
		{
			name: "cycle deep in chain",
			graph: map[string][]string{
				"a": {"b"}, "b": {"c"}, "c": {"d"}, "d": {"e"}, "e": {"c"},
			},
			want: true,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := HasCycle(c.graph); got != c.want {
				t.Errorf("HasCycle(%v) = %v, want %v", c.graph, got, c.want)
			}
		})
	}
}
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```go
package challenge

func HasCycle(graph map[string][]string) bool {
	if graph == nil || len(graph) == 0 {
		return false
	}

	// States: 0 = unvisited, 1 = visiting (in current DFS path), 2 = visited (completed)
	state := make(map[string]int)

	var dfs func(node string) bool
	dfs = func(node string) bool {
		// If node not in graph, it has no outgoing edges, so no cycle from here
		if _, exists := state[node]; !exists {
			return false
		}

		// If we've already finished processing this node, no cycle through it
		if state[node] == 2 {
			return false
		}

		// If we're currently visiting this node, we found a cycle
		if state[node] == 1 {
			return true
		}

		// Mark as visiting
		state[node] = 1

		// Visit all neighbors
		for _, neighbor := range graph[node] {
			if dfs(neighbor) {
				return true
			}
		}

		// Mark as fully visited
		state[node] = 2
		return false
	}

	// Check each node in the graph for cycles
	for node := range graph {
		if state[node] == 0 { // Unvisited
			if dfs(node) {
				return true
			}
		}
	}

	return false
}
```

Test output (stdout)

eaf),_no_cycle","Output":"--- PASS: TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835474635+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835475515+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle"}
{"Time":"2026-07-01T22:48:06.835476365+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle","Output":"=== RUN   TestHasCycle/duplicate_edges,_no_cycle\n"}
{"Time":"2026-07-01T22:48:06.835478325+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle","Output":"--- PASS: TestHasCycle/duplicate_edges,_no_cycle (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835479365+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835481195+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle"}
{"Time":"2026-07-01T22:48:06.835481985+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Output":"=== RUN   TestHasCycle/duplicate_edges_forming_cycle\n"}
{"Time":"2026-07-01T22:48:06.835483225+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Output":"    solution_test.go:110: HasCycle(map[a:[b b] b:[a a]]) = false, want true\n"}
{"Time":"2026-07-01T22:48:06.835484355+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Output":"--- FAIL: TestHasCycle/duplicate_edges_forming_cycle (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835485335+02:00","Action":"fail","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835486265+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle"}
{"Time":"2026-07-01T22:48:06.835487055+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle","Output":"=== RUN   TestHasCycle/long_chain_no_cycle\n"}
{"Time":"2026-07-01T22:48:06.835488155+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle","Output":"--- PASS: TestHasCycle/long_chain_no_cycle (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835490115+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835491265+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain"}
{"Time":"2026-07-01T22:48:06.835492085+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Output":"=== RUN   TestHasCycle/cycle_deep_in_chain\n"}
{"Time":"2026-07-01T22:48:06.835493125+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Output":"    solution_test.go:110: HasCycle(map[a:[b] b:[c] c:[d] d:[e] e:[c]]) = false, want true\n"}
{"Time":"2026-07-01T22:48:06.835494245+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Output":"--- FAIL: TestHasCycle/cycle_deep_in_chain (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835495515+02:00","Action":"fail","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835496595+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle","Output":"--- FAIL: TestHasCycle (0.00s)\n"}
{"Time":"2026-07-01T22:48:06.835497635+02:00","Action":"fail","Package":"challenge","Test":"TestHasCycle","Elapsed":0}
{"Time":"2026-07-01T22:48:06.835498615+02:00","Action":"output","Package":"challenge","Output":"FAIL\n"}
{"Time":"2026-07-01T22:48:06.835675266+02:00","Action":"output","Package":"challenge","Output":"FAIL\tchallenge\t0.001s\n"}
{"Time":"2026-07-01T22:48:06.835685386+02:00","Action":"fail","Package":"challenge","Elapsed":0.001}
Self-repair attempts
[
  {
    "total": 1,
    "answer": "```go\npackage challenge\n\nfunc HasCycle(graph map[string][]string) bool {\n\tif graph == nil || len(graph) == 0 {\n\t\treturn false\n\t}\n\n\t// States: 0 = unvisited, 1 = visiting (in current DFS path), 2 = visited (completed)\n\tstate := make(map[string]int)\n\n\tvar dfs func(node string) bool\n\tdfs = func(node string) bool {\n\t\t// If node not in graph, it has no outgoing edges, so no cycle from here\n\t\tif _, exists := state[node]; !exists {\n\t\t\treturn false\n\t\t}\n\n\t\t// If we've already finished processing this node, no cycle through it\n\t\tif state[node] == 2 {\n\t\t\treturn false\n\t\t}\n\n\t\t// If we're currently visiting this node, we found a cycle\n\t\tif state[node] == 1 {\n\t\t\treturn true\n\t\t}\n\n\t\t// Mark as visiting\n\t\tstate[node] = 1\n\n\t\t// Visit all neighbors\n\t\tfor _, neighbor := range graph[node] {\n\t\t\tif dfs(neighbor) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\t// Mark as fully visited\n\t\tstate[node] = 2\n\t\treturn false\n\t}\n\n\t// Check each node in the graph for cycles\n\tfor node := range graph {\n\t\tif state[node] == 0 { // Unvisited\n\t\t\tif dfs(node) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n```",
    "passed": 0,
    "test_error": "lse, want true\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835484355+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/duplicate_edges_forming_cycle\",\"Output\":\"--- FAIL: TestHasCycle/duplicate_edges_forming_cycle (0.00s)\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835485335+02:00\",\"Action\":\"fail\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/duplicate_edges_forming_cycle\",\"Elapsed\":0}\n{\"Time\":\"2026-07-01T22:48:06.835486265+02:00\",\"Action\":\"run\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/long_chain_no_cycle\"}\n{\"Time\":\"2026-07-01T22:48:06.835487055+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/long_chain_no_cycle\",\"Output\":\"=== RUN   TestHasCycle/long_chain_no_cycle\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835488155+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/long_chain_no_cycle\",\"Output\":\"--- PASS: TestHasCycle/long_chain_no_cycle (0.00s)\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835490115+02:00\",\"Action\":\"pass\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/long_chain_no_cycle\",\"Elapsed\":0}\n{\"Time\":\"2026-07-01T22:48:06.835491265+02:00\",\"Action\":\"run\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/cycle_deep_in_chain\"}\n{\"Time\":\"2026-07-01T22:48:06.835492085+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/cycle_deep_in_chain\",\"Output\":\"=== RUN   TestHasCycle/cycle_deep_in_chain\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835493125+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/cycle_deep_in_chain\",\"Output\":\"    solution_test.go:110: HasCycle(map[a:[b] b:[c] c:[d] d:[e] e:[c]]) = false, want true\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835494245+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/cycle_deep_in_chain\",\"Output\":\"--- FAIL: TestHasCycle/cycle_deep_in_chain (0.00s)\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835495515+02:00\",\"Action\":\"fail\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle/cycle_deep_in_chain\",\"Elapsed\":0}\n{\"Time\":\"2026-07-01T22:48:06.835496595+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle\",\"Output\":\"--- FAIL: TestHasCycle (0.00s)\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835497635+02:00\",\"Action\":\"fail\",\"Package\":\"challenge\",\"Test\":\"TestHasCycle\",\"Elapsed\":0}\n{\"Time\":\"2026-07-01T22:48:06.835498615+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Output\":\"FAIL\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835675266+02:00\",\"Action\":\"output\",\"Package\":\"challenge\",\"Output\":\"FAIL\\tchallenge\\t0.001s\\n\"}\n{\"Time\":\"2026-07-01T22:48:06.835685386+02:00\",\"Action\":\"fail\",\"Package\":\"challenge\",\"Elapsed\":0.001}"
  },
  {
    "total": 1,
    "answer": "Looking at the test failures, I can see two issues:\n\n1. The `duplicate_edges_forming_cycle` test is failing (want true, got false)\n2. The `cycle_deep_in_chain` test is failing with `HasCycle(map[a:[b] b:[c] c:[d] d:[e] e:[c]]) = false, want true`\n\nThe second failure reveals the core issue: when a node like \"e\" points to \"c\", and \"c\" points to \"d\", etc., forming a cycle `c -> d -> e -> c`, my algorithm isn't detecting it properly.\n\nThe problem is that I'm only checking nodes that exist as keys in the graph, but I need to handle the case where a node might not be a key in the map (has no outgoing edges) but could still be part of a cycle path.\n\nLooking more closely at the `cycle_deep_in_chain` example:\n- `map[a:[b] b:[c] c:[d] d:[e] e:[c]]`\n- This forms a cycle: `c -> d -> e -> c`\n\nThe issue is that my DFS state tracking isn't working correctly. Let me trace through:\n1. Start at \"a\", mark as visiting (1)\n2. Go to \"b\", mark as visiting (1)  \n3. Go to \"c\", mark as visiting (1)\n4. Go to \"d\", mark as visiting (1)\n5. Go to \"e\", mark as visiting (1)\n6. From \"e\" go to \"c\" - \"c\" is already in state 1 (visiting), so should return true\n\nWait, that should work. Let me re-examine the logic.\n\nActually, I think the issue might be with how I'm handling nodes that aren't keys in the graph. But in this case, all nodes in the cycle are keys.\n\nLet me check the duplicate edges test - it probably has something like `map[a:[b,b]]` or similar, which shouldn't affect cycle detection but might expose an issue.\n\nThe real problem is likely that I'm checking `if _, exists := state[node]; !exists` when I should be checking if the node exists in the graph map instead. Let me fix this:\n\n```go\npackage challenge\n\nfunc HasCycle(graph map[string][]string) bool {\n\tif graph == nil || len(graph) == 0 {\n\t\treturn false\n\t}\n\n\t// States: 0 = unvisited, 1 = visiting (in current DFS path), 2 = visited (completed)\n\tstate := make(map[string]int)\n\n\tvar dfs func(node string) bool\n\tdfs = func(node string) bool {\n\t\t// If node is not in the graph, it has no outgoing edges, so no cycle from here\n\t\tif _, exists := graph[node]; !exists {\n\t\t\treturn false\n\t\t}\n\n\t\t// If we've already finished processing this node, no cycle through it\n\t\tif state[node] == 2 {\n\t\t\treturn false\n\t\t}\n\n\t\t// If we're currently visiting this node, we found a cycle\n\t\tif state[node] == 1 {\n\t\t\treturn true\n\t\t}\n\n\t\t// Mark as visiting\n\t\tstate[node] = 1\n\n\t\t// Visit all neighbors\n\t\tfor _, neighbor := range graph[node] {\n\t\t\tif dfs(neighbor) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\t// Mark as fully visited\n\t\tstate[node] = 2\n\t\treturn false\n\t}\n\n\t// Check each node in the graph for cycles\n\tfor node := range graph {\n\t\tif state[node] == 0 { // Unvisited\n\t\t\tif dfs(node) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n```",
    "passed": 1,
    "test_error": ""
  }
]