Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Sources/SwiftGraph/Graph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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>) -> 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<Int>,
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
}

}
40 changes: 40 additions & 0 deletions Tests/SwiftGraphTests/SwiftGraphTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<Int> = [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<String> {
// 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<String>.self, from: serializedGraph.data(using: .utf8)!)
}
}