go-03-detect-cycle
1.000
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 {
// Track node states: 0 = unvisited, 1 = visiting, 2 = visited
state := make(map[string]int)
// Helper DFS function to detect cycles
var dfs func(node string) bool
dfs = func(node string) bool {
// If node is currently being visited, we found a cycle
if state[node] == 1 {
return true
}
// If node is already fully visited, no cycle from this path
if state[node] == 2 {
return false
}
// Mark node as being visited
state[node] = 1
// Visit all neighbors
for _, neighbor := range graph[node] {
if dfs(neighbor) {
return true
}
}
// Mark node as fully visited
state[node] = 2
return false
}
// Check all nodes in the graph
for node := range graph {
if state[node] == 0 {
if dfs(node) {
return true
}
}
}
return false
}
```Test output (stdout)
ackage":"challenge","Test":"TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle"}
{"Time":"2026-07-02T04:34:13.433429478+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle","Output":"=== RUN TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle\n"}
{"Time":"2026-07-02T04:34:13.433431108+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle","Output":"--- PASS: TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle (0.00s)\n"}
{"Time":"2026-07-02T04:34:13.433432278+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/neighbor_not_a_key_(implicit_leaf),_no_cycle","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433433298+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle"}
{"Time":"2026-07-02T04:34:13.433434538+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle","Output":"=== RUN TestHasCycle/duplicate_edges,_no_cycle\n"}
{"Time":"2026-07-02T04:34:13.433435648+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-02T04:34:13.433436678+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/duplicate_edges,_no_cycle","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433437798+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle"}
{"Time":"2026-07-02T04:34:13.433439028+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Output":"=== RUN TestHasCycle/duplicate_edges_forming_cycle\n"}
{"Time":"2026-07-02T04:34:13.433441008+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Output":"--- PASS: TestHasCycle/duplicate_edges_forming_cycle (0.00s)\n"}
{"Time":"2026-07-02T04:34:13.433442308+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/duplicate_edges_forming_cycle","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433443258+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle"}
{"Time":"2026-07-02T04:34:13.433444368+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle","Output":"=== RUN TestHasCycle/long_chain_no_cycle\n"}
{"Time":"2026-07-02T04:34:13.433445448+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-02T04:34:13.433447078+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/long_chain_no_cycle","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433447988+02:00","Action":"run","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain"}
{"Time":"2026-07-02T04:34:13.433448928+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Output":"=== RUN TestHasCycle/cycle_deep_in_chain\n"}
{"Time":"2026-07-02T04:34:13.433450048+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Output":"--- PASS: TestHasCycle/cycle_deep_in_chain (0.00s)\n"}
{"Time":"2026-07-02T04:34:13.433450998+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle/cycle_deep_in_chain","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433452118+02:00","Action":"output","Package":"challenge","Test":"TestHasCycle","Output":"--- PASS: TestHasCycle (0.00s)\n"}
{"Time":"2026-07-02T04:34:13.433453148+02:00","Action":"pass","Package":"challenge","Test":"TestHasCycle","Elapsed":0}
{"Time":"2026-07-02T04:34:13.433454178+02:00","Action":"output","Package":"challenge","Output":"PASS\n"}
{"Time":"2026-07-02T04:34:13.433634117+02:00","Action":"output","Package":"challenge","Output":"ok \tchallenge\t0.001s\n"}
{"Time":"2026-07-02T04:34:13.433640067+02:00","Action":"pass","Package":"challenge","Elapsed":0.001}
Reasoning (chain-of-thought)
The task is to implement a cycle detection algorithm for a directed graph in Go. The graph is represented as an adjacency map where each key is a node and its value is a slice of nodes it has directed edges to.
To detect cycles in a directed graph, I can use Depth First Search (DFS) with three states for each node:
1. **Unvisited** - Node has not been visited yet
2. **Visiting** - Node is currently being processed (in the current DFS path)
3. **Visited** - Node has been completely processed
If during DFS we encounter a node that is in the "Visiting" state, we've found a cycle.
Here's my approach:
1. Track visited nodes with three states
2. For each unvisited node in the graph, start a DFS
3. During DFS, if we encounter a node that's currently being visited, we have a cycle
4. If we complete DFS for a node without finding a cycle, mark it as fully visited
Let me implement this:
```go
package challenge
func HasCycle(graph map[string][]string) bool {
// Track node states: 0 = unvisited, 1 = visiting, 2 = visited
state := make(map[string]int)
// Helper DFS function
var dfs func(node string) bool
dfs = func(node string) bool {
// If node is currently being visited, we found a cycle
if state[node] == 1 {
return true
}
// If node is already fully visited, no cycle from this path
if state[node] == 2 {
return false
}
// Mark node as being visited
state[node] = 1
// Visit all neighbors
for _, neighbor := range graph[node] {
if dfs(neighbor) {
return true
}
}
// Mark node as fully visited
state[node] = 2
return false
}
// Check all nodes in the graph
for node := range graph {
if state[node] == 0 {
if dfs(node) {
return true
}
}
}
return false
}
```
I need to handle nodes that appear as neighbors but aren't keys in the graph. These nodes have no outgoing edges, so they won't cause cycles. My current implementation handles this correctly since `graph[neighbor]` will return an empty slice for such nodes.
However, I should also consider nodes that are only reachable through traversal but never appear as keys. Since I'm iterating over all keys in the graph, I'll catch cycles starting from any node that has outgoing edges.
Let me trace through the examples to verify:
Example 1: `{"a": {"b"}, "b": {"c"}, "c": {}}`
- Start at "a", mark as visiting
- Visit "b", mark as visiting
- Visit "c", mark as visiting
- "c" has no neighbors, mark as visited
- Backtrack to "b", mark as visited
- Backtrack to "a", mark as visited
- No cycle found โ false โ
Example 2: `{"a": {"a"}}`
- Start at "a", mark as visiting
- Visit "a" (neighbor), it's already visiting โ cycle found โ true โ
Example 3: `{"a": {"b"}, "b": {"c"}, "c": {"a"}}`
- Start at "a", mark as visiting
- Visit "b", mark as visiting
- Visit "c", mark as visiting
- Visit "a" (neighbor), it's already visiting โ cycle found โ true โ
Example 4: `{}`
- No nodes to iterate โ false โ
The implementation handles all cases correctly. I should also consider edge cases like nil maps, which my code handles since iterating over a nil map does nothing.
One thing to verify: when accessing `graph[node]` for a node that's not a key, Go returns an empty slice, which is safe. This means nodes that only appear as neighbors but not as keys will have no outgoing edges, which is correct.
I'm ready to write the final solution.