diff --git a/Sources/SwiftGraph/Graph.swift b/Sources/SwiftGraph/Graph.swift index 7b3ff54..ad32d0a 100644 --- a/Sources/SwiftGraph/Graph.swift +++ b/Sources/SwiftGraph/Graph.swift @@ -303,3 +303,91 @@ extension Graph { } } +extension Graph { + /// Find all of the neighbors of a vertex at a given index. + /// + /// - parameter index: The index for the vertex to find the neighbors of. + /// - returns: An array of the neighbor vertex indices. + public func neighborIndicesForIndex(_ index: Int) -> [Int] { + return edges[index].map({$0.v}) + } + + /// Find out if a route exists from one vertex to another, using a DFS. + /// + /// - parameter fromIndex: The index of the starting vertex. + /// - parameter toIndex: The index of the ending vertex. + /// - returns: `true` if a path exists + func pathExists(fromIndex: Int, toIndex: Int) -> Bool { + var visited: [Bool] = [Bool](repeating: false, count: vertexCount) + var stack: [Int] = [] + stack.append(fromIndex) + while !stack.isEmpty { + if let v: Int = stack.popLast() { + if (visited[v]) { + continue + } + visited[v] = true + if v == toIndex { + // we've found the destination + return true + } + for e in edgesForIndex(v) { + if !visited[e.v] { + stack.append(e.v) + } + } + } + } + return false // no solution found + } + + /// This is an exhaustive search to find out how many paths there are between two vertices. + /// + /// - parameter fromIndex: the index of the starting vertex + /// - parameter toIndex: the index of the destination vertex + /// - parameter visited: a set of vertex indices which will be considered to have been visited already + /// - returns: the number of paths that exist going from the start to the destinatin + func countPaths(fromIndex startIndex: Int, toIndex endIndex: Int, visited: inout Set) -> Int { + if startIndex == endIndex { return 1 } + visited.insert(startIndex) + var total = 0 + for n in neighborIndicesForIndex(startIndex) where !visited.contains(n) { + total += countPaths(fromIndex: n, toIndex: endIndex, visited: &visited) + } + visited.remove(startIndex) + return total + } + + /// This is an exhaustive search to find out how many paths there are between two vertices. + /// The search is optimized by not bothering to compute paths for known dead ends. + /// + /// - parameter fromIndex: the index of the starting vertex + /// - parameter toIndex: the index of the destination vertex + /// - parameter visited: a set of vertex indices which will be considered to have been visited already + /// - parameter reachability: a dictionary where the key is a vertex index and the value is whether or not a path exists from that vertex to the destination + /// - returns: the number of paths that exist going from the start to the destinatin + func countPaths(fromIndex startIndex: Int, + toIndex endIndex: Int, + visited: inout Set, + reachability: [Int: Bool]) -> Int { + if startIndex == endIndex { return 1 } + guard reachability[startIndex] ?? false else { return 0 } + visited.insert(startIndex) + var total = 0 + for n in neighborIndicesForIndex(startIndex) where !visited.contains(n) && (reachability[n] ?? false) { + total += countPaths(fromIndex: n, toIndex: endIndex, visited: &visited, reachability: reachability) + } + visited.remove(startIndex) + return total + } + + /// Computes whether or not a given vertex (by index) is reachable, for every vertex in the graph. + func reachabilityOf(_ index: Int) -> [Int: Bool] { + var answers: [Int: Bool] = [:] + for vi in vertices.indices { + answers[vi] = pathExists(fromIndex: vi, toIndex: index) + } + return answers + } + +} diff --git a/Tests/SwiftGraphTests/SwiftGraphTests.swift b/Tests/SwiftGraphTests/SwiftGraphTests.swift index 09cf198..05b0506 100644 --- a/Tests/SwiftGraphTests/SwiftGraphTests.swift +++ b/Tests/SwiftGraphTests/SwiftGraphTests.swift @@ -16,6 +16,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation import XCTest @testable import SwiftGraph @@ -132,4 +133,43 @@ class SwiftGraphTests: XCTestCase { XCTAssertFalse(graph.edgeExists(fromIndex: 2, toIndex: 3)) XCTAssertFalse(graph.edgeExists(fromIndex: 3, toIndex: 2)) } + + // a trivial smoke test to exercise the methods added to Graph to + // perform exhaustive searches for paths. + func testExhaustivePathCounting() throws { + let graph = try getAoCGraph() + guard let fftIndex = graph.indexOfVertex("fft") else { + XCTFail("Bad graph, no fft") + return + } + guard let dacIndex = graph.indexOfVertex("dac") else { + XCTFail("Bad graph, no dac") + return + } + guard let outIndex = graph.indexOfVertex("out") else { + XCTFail("Bad graph, no out") + return + } + guard let svrIndex = graph.indexOfVertex( "svr" ) else { + XCTFail("Bad graph, no svr") + return + } + var visited: Set = [svrIndex, outIndex] + XCTAssertEqual(0, graph.countPaths(fromIndex: dacIndex, toIndex: fftIndex, visited: &visited)) + let outReachability = graph.reachabilityOf(outIndex) + visited = [svrIndex, fftIndex] + XCTAssertEqual(2, + graph.countPaths(fromIndex: dacIndex, + toIndex: outIndex, + visited: &visited, + reachability: outReachability)) + } + + fileprivate func getAoCGraph() throws -> UnweightedGraph { + // this is the sample graph for part 2 of the 2025 Advent of Code challenge, day 11. + let serializedGraph = """ + {"vertices":["hhh","ddd","fft","ccc","svr","fff","eee","aaa","bbb","hub","ggg","dac","tty","out"],"edges":[[{"u":0,"v":13,"directed":true}],[{"u":1,"v":9,"directed":true}],[{"u":2,"v":3,"directed":true}],[{"u":3,"v":1,"directed":true},{"u":3,"v":6,"directed":true}],[{"u":4,"v":7,"directed":true},{"u":4,"v":8,"directed":true}],[{"u":5,"v":10,"directed":true},{"u":5,"v":0,"directed":true}],[{"u":6,"v":11,"directed":true}],[{"u":7,"v":2,"directed":true}],[{"u":8,"v":12,"directed":true}],[{"u":9,"v":5,"directed":true}],[{"u":10,"v":13,"directed":true}],[{"u":11,"v":5,"directed":true}],[{"u":12,"v":3,"directed":true}],[]]} + """ + return try JSONDecoder().decode(UnweightedGraph.self, from: serializedGraph.data(using: .utf8)!) + } }