Yog.FSharp
Getting Started Examples API Reference GitHub

Cave Path Counting (Backtracking DFS)

This example demonstrates a complex path-finding problem on a graph that requires custom depth-first search (DFS) with backtracking.

Problem

Count all possible paths from "start" to "end" in a cave system. - Large caves (UPPERCASE) can be visited multiple times. - Small caves (lowercase) can only be visited once.

#I "../../src/Yog.FSharp/bin/Debug/net10.0"
#r "Yog.FSharp.dll"

open Yog.Model

// Model the cave system
let caveGraph =
    empty Undirected
    |> addNode 0 "start"
    |> addNode 1 "A"
    |> addNode 2 "b"
    |> addNode 3 "c"
    |> addNode 4 "d"
    |> addNode 5 "end"
    |> addEdge 0 1 ()
    |> addEdge 0 2 ()
    |> addEdge 1 3 ()
    |> addEdge 1 2 ()
    |> addEdge 2 4 ()
    |> addEdge 1 5 ()
    |> addEdge 4 5 ()

// Helper to determine if a cave is small
let isSmall (name: string) = name.ToLower() = name

// Custom recursive DFS with backtracking
let rec countPaths current (visitedSmall: Set<string>) canRevisitOne =
    let caveName = caveGraph.Nodes.[current]
    
    if caveName = "end" then 1
    else
        successorIds current caveGraph
        |> List.fold (fun total neighborId ->
            let neighborName = caveGraph.Nodes.[neighborId]
            let isSmallCave = isSmall neighborName
            let alreadyVisited = Set.contains neighborName visitedSmall
            
            match neighborName, isSmallCave, alreadyVisited with
            | "start", _, _ -> total
            | _, false, _ -> 
                total + countPaths neighborId visitedSmall canRevisitOne
            | _, true, false ->
                let nextVisited = Set.add neighborName visitedSmall
                total + countPaths neighborId nextVisited canRevisitOne
            | _, true, true when canRevisitOne ->
                total + countPaths neighborId visitedSmall false
            | _ -> total
        ) 0

printfn "--- Cave Path Counting ---"

let totalPaths = countPaths 0 (Set.singleton "start") true
printfn "Found %d valid paths through the cave system." totalPaths

Output

--- Cave Path Counting ---
Found 33 valid paths through the cave system.
val caveGraph: obj
val isSmall: name: string -> bool
val name: string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
System.String.ToLower() : string
System.String.ToLower(culture: System.Globalization.CultureInfo) : string
val countPaths: current: 'a -> visitedSmall: Set<string> -> canRevisitOne: bool -> int
val current: 'a
val visitedSmall: Set<string>
Multiple items
module Set from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IStructuralEquatable interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: 'T seq -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool override Equals: objnull -> bool ...

--------------------
new: elements: 'T seq -> Set<'T>
val canRevisitOne: bool
val caveName: string
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
val fold<'T,'State> : folder: ('State -> 'T -> 'State) -> state: 'State -> list: 'T list -> 'State
val total: int
val neighborId: 'a
val neighborName: string
val isSmallCave: bool
val alreadyVisited: bool
val contains: element: 'T -> set: Set<'T> -> bool (requires comparison)
val nextVisited: Set<string>
val add: value: 'T -> set: Set<'T> -> Set<'T> (requires comparison)
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val totalPaths: int
val singleton: value: 'T -> Set<'T> (requires comparison)