Skip to content

Commit bf70e67

Browse files
authored
Merge pull request #93 from sbeitzel/feature/exhaustive_dfs
Add methods for efficient counting of paths between vertices.
2 parents 19101a3 + 60108d1 commit bf70e67

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

Sources/SwiftGraph/Graph.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,91 @@ extension Graph {
303303
}
304304
}
305305

306+
extension Graph {
307+
/// Find all of the neighbors of a vertex at a given index.
308+
///
309+
/// - parameter index: The index for the vertex to find the neighbors of.
310+
/// - returns: An array of the neighbor vertex indices.
311+
public func neighborIndicesForIndex(_ index: Int) -> [Int] {
312+
return edges[index].map({$0.v})
313+
}
314+
315+
/// Find out if a route exists from one vertex to another, using a DFS.
316+
///
317+
/// - parameter fromIndex: The index of the starting vertex.
318+
/// - parameter toIndex: The index of the ending vertex.
319+
/// - returns: `true` if a path exists
320+
public func pathExists(fromIndex: Int, toIndex: Int) -> Bool {
321+
var visited: [Bool] = [Bool](repeating: false, count: vertexCount)
322+
var stack: [Int] = []
323+
stack.append(fromIndex)
324+
while !stack.isEmpty {
325+
if let v = stack.popLast() {
326+
if visited[v] {
327+
continue
328+
}
329+
visited[v] = true
330+
if v == toIndex {
331+
// we've found the destination
332+
return true
333+
}
334+
for e in edgesForIndex(v) {
335+
if !visited[e.v] {
336+
stack.append(e.v)
337+
}
338+
}
339+
}
340+
}
341+
return false // no solution found
342+
}
343+
344+
/// This is an exhaustive search to find out how many paths there are between two vertices.
345+
///
346+
/// - parameter fromIndex: the index of the starting vertex
347+
/// - parameter toIndex: the index of the destination vertex
348+
/// - parameter visited: a set of vertex indices which will be considered to have been visited already
349+
/// - returns: the number of paths that exist going from the start to the destination
350+
public func countPaths(fromIndex startIndex: Int, toIndex endIndex: Int, visited: inout Set<Int>) -> Int {
351+
if startIndex == endIndex { return 1 }
352+
visited.insert(startIndex)
353+
var total = 0
354+
for n in neighborIndicesForIndex(startIndex) where !visited.contains(n) {
355+
total += countPaths(fromIndex: n, toIndex: endIndex, visited: &visited)
356+
}
357+
visited.remove(startIndex)
358+
return total
359+
}
360+
361+
/// This is an exhaustive search to find out how many paths there are between two vertices.
362+
/// The search is optimized by not bothering to compute paths for known dead ends.
363+
///
364+
/// - parameter fromIndex: the index of the starting vertex
365+
/// - parameter toIndex: the index of the destination vertex
366+
/// - parameter visited: a set of vertex indices which will be considered to have been visited already
367+
/// - parameter reachability: a dictionary mapping vertex indices to a Boolean indicating whether a path exists (`true`) or does not exist (`false`) from that vertex to the destination
368+
/// - returns: the number of paths that exist going from the start to the destination
369+
public func countPaths(fromIndex startIndex: Int,
370+
toIndex endIndex: Int,
371+
visited: inout Set<Int>,
372+
reachability: [Int: Bool]) -> Int {
373+
if startIndex == endIndex { return 1 }
374+
guard reachability[startIndex] ?? false else { return 0 }
375+
visited.insert(startIndex)
376+
var total = 0
377+
for n in neighborIndicesForIndex(startIndex) where !visited.contains(n) && (reachability[n] ?? false) {
378+
total += countPaths(fromIndex: n, toIndex: endIndex, visited: &visited, reachability: reachability)
379+
}
380+
visited.remove(startIndex)
381+
return total
382+
}
383+
384+
/// Computes whether or not a given vertex (by index) is reachable, for every vertex in the graph.
385+
public func reachabilityOf(_ index: Int) -> [Int: Bool] {
386+
var answers: [Int: Bool] = [:]
387+
for vi in vertices.indices {
388+
answers[vi] = pathExists(fromIndex: vi, toIndex: index)
389+
}
390+
return answers
391+
}
392+
393+
}

Tests/SwiftGraphTests/SwiftGraphTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// See the License for the specific language governing permissions and
1717
// limitations under the License.
1818

19+
import Foundation
1920
import XCTest
2021
@testable import SwiftGraph
2122

@@ -132,4 +133,43 @@ class SwiftGraphTests: XCTestCase {
132133
XCTAssertFalse(graph.edgeExists(fromIndex: 2, toIndex: 3))
133134
XCTAssertFalse(graph.edgeExists(fromIndex: 3, toIndex: 2))
134135
}
136+
137+
// a trivial smoke test to exercise the methods added to Graph to
138+
// perform exhaustive searches for paths.
139+
func testExhaustivePathCounting() throws {
140+
let graph = try getAoCGraph()
141+
guard let fftIndex = graph.indexOfVertex("fft") else {
142+
XCTFail("Bad graph, no fft")
143+
return
144+
}
145+
guard let dacIndex = graph.indexOfVertex("dac") else {
146+
XCTFail("Bad graph, no dac")
147+
return
148+
}
149+
guard let outIndex = graph.indexOfVertex("out") else {
150+
XCTFail("Bad graph, no out")
151+
return
152+
}
153+
guard let svrIndex = graph.indexOfVertex("svr") else {
154+
XCTFail("Bad graph, no svr")
155+
return
156+
}
157+
var visited: Set<Int> = [svrIndex, outIndex]
158+
XCTAssertEqual(0, graph.countPaths(fromIndex: dacIndex, toIndex: fftIndex, visited: &visited))
159+
let outReachability = graph.reachabilityOf(outIndex)
160+
visited = [svrIndex, fftIndex]
161+
XCTAssertEqual(2,
162+
graph.countPaths(fromIndex: dacIndex,
163+
toIndex: outIndex,
164+
visited: &visited,
165+
reachability: outReachability))
166+
}
167+
168+
fileprivate func getAoCGraph() throws -> UnweightedGraph<String> {
169+
// this is the sample graph for part 2 of the 2025 Advent of Code challenge, day 11.
170+
let serializedGraph = """
171+
{"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}],[]]}
172+
"""
173+
return try JSONDecoder().decode(UnweightedGraph<String>.self, from: serializedGraph.data(using: .utf8)!)
174+
}
135175
}

0 commit comments

Comments
 (0)