diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3f8110b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 László Teveli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..d20702d --- /dev/null +++ b/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "e8d81c4ed867478e73dccb21d2e981e1710794cff51372edb0a3508f1639cfd5", + "pins" : [ + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..dd60122 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Graphs", + products: [ + .library( + name: "Graphs", + targets: ["Graphs"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), + .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0") + ], + targets: [ + .target( + name: "Graphs", + dependencies: [ + .product(name: "Collections", package: "swift-collections"), + .product(name: "Algorithms", package: "swift-algorithms") + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), + .testTarget( + name: "GraphsTests", + dependencies: ["Graphs"], + swiftSettings: [.swiftLanguageMode(.v6)] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..532ad65 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# Graph Library for Swift + +## Overview + +This Swift library provides a composable and extensible foundation for working with graphs. +Whether you're constructing binary trees, performing complex graph traversals, or optimizing pathfinding algorithms on weighted graphs, this library offers the dynamic flexibility needed for a wide range of graph-related use cases. + +While some features are still in early development stages, the project is actively evolving, and contributions are highly encouraged. + +## Key Features + +- **Multiple Graph Types**: Supports general graphs, binary graphs, grid graphs, and lazy graphs (on-demand edge computation). +- **Weighted Graphs**: Handles graphs with weighted edges, enabling specialized algorithms like shortest pathfinding. +- **Traversals**: Breadth-First Search (BFS), Depth-First Search (DFS), with support for preorder, inorder, and postorder traversals. +- **Traversal Strategies**: Includes unique node visiting, and depth, path, and cost tracking during traversals. +- **Shortest Path Algorithms**: Dijkstra, Bellman-Ford, and A* algorithms for finding efficient routes. +- **Eulerian and Hamiltonian Paths/Cycles**: Support for backtracking, heuristic-based, and Hierholzer's algorithm for Eulerian paths. +- **Max Flow/Min Cut Algorithms**: Ford-Fulkerson, Edmonds-Karp, and Dinic's algorithms for network flow analysis. +- **Minimum Spanning Tree Algorithms**: Kruskal's, Prim's, and Borůvka's algorithms for constructing minimum spanning trees. +- **Strongly Connected Components**: Kosaraju’s and Tarjan’s algorithms for identifying strongly connected components. +- **Graph Coloring**: Greedy algorithm for efficient node coloring. +- **Maximum Bipartite Matching**: Hopcroft-Karp algorithm for bipartite matching. + +### Example Usage + +```swift +let graph = Graph(edges: [ + "Root": ["A", "B", "C"], + "B": ["X", "Y", "Z"] +]) + +graph.traverse(from: "Root", strategy: .bfs()) +graph.traverse(from: "Root", strategy: .dfs()) + +graph.traverse(from: "Root", strategy: .bfs(.trackPath())) +graph.traverse(from: "Root", strategy: .dfs(order: .postorder())) +graph.traverse(from: "Root", strategy: .dfs().visitEachNodeOnce()) + +graph.shortestPath(from: "Root", to: "Z", using: .dijkstra()) // or .aStar() or .bellmanFord() +graph.shortestPaths(from: "Root", using: .dijkstra()) // or .bellmanFord() +graph.shortestPathsForAllPairs(using: .floydWarshall()) // or .johnson() +graph.minimumSpanningTree(using: .kruskal()) // or .prim() or .boruvka() +graph.maximumFlow(using: .fordFulkerson()) // or .edmondsKarp() or .dinic() +graph.stronglyConnectedComponents(using: .kosaraju()) // or .tarjan() + +graph.colorNodes() +graph.isCyclic() +graph.isTree() +graph.isConnected() +graph.isPlanar() +graph.isBipartite() +graph.topologicalSort() + +let lazyGraph = LazyGraph { node in + dynamicNeighbors(of: node) +} + +let gridGraph = GridGraph(grid: [ + ["A", "B", "C", "D", "E"], + ["F", "G", "H", "I", "J"], + ["K", "L", "M", "N", "O"], + ["P", "Q", "R", "S", "T"], + ["U", "V", "W", "X", "Y"] +], availableDirections: .orthogonal).weightedByDistance() + +gridGraph.shortestPath(from: GridPosition(x: 0, y: 0), to: GridPosition(x: 4, y: 4), using: .aStar(heuristic: .euclideanDistance(of: \.coordinates))) +gridGraph.shortestPaths(from: GridPosition(x: 0, y: 0)) +gridGraph.shortestPathsForAllPairs() + +gridGraph.eulerianCycle() +gridGraph.eulerianPath() + +gridGraph.hamiltonianCycle() +gridGraph.hamiltonianPath() +gridGraph.hamiltonianPath(from: GridPosition(x: 0, y: 0)) +gridGraph.hamiltonianPath(from: GridPosition(x: 0, y: 0), to: GridPosition(x: 4, y: 4)) + +let binaryGraph = BinaryGraph(edges: [ + "Root": (lhs: "A", rhs: "B"), + "A": (lhs: "X", rhs: "Y"), + "Y": (lhs: nil, rhs: "Z") +]) +binaryGraph.traverse(from: "Root", strategy: .dfs(order: .inorder())) + +let bipartite = Graph(edges: [ + GraphEdge(source: "u1", destination: "v1"), + GraphEdge(source: "u1", destination: "v2"), + GraphEdge(source: "u2", destination: "v1"), + GraphEdge(source: "u3", destination: "v2"), + GraphEdge(source: "u3", destination: "v3"), +]).bipartite(leftPartition: ["u1", "u2", "u3"], rightPartition: ["v1", "v2", "v3"]) + +bipartite.maximumMatching(using: .hopcroftKarp()) +``` + +## Design Considerations + +### Generic Structure + +The library is built on a fully generic structure, allowing all nodes and edges to be defined as generic types without constraints. +This flexibility enables seamless integration of various algorithms on specialized graphs, such as weighted graphs. +By using generics, the library ensures that algorithms remain broadly applicable while optimized for specific graph types. + +For example, binary graphs can leverage specialized inorder depth-first search, while layered traversal strategies—such as unique node visits or path tracking—can be easily added to any algorithm. +Generic constraints help formalize requirements, ensuring algorithms like Dijkstra's avoid negative weights for optimal performance. + +### Composable Components and Algorithms + +This library is designed with composability in mind. +Similar to how Swift’s standard library transforms collections (e.g., `ReversedCollection`), this library provides efficient graph transformations such as `TransposedGraph` or `UndirectedGraph`. +Algorithms can be layered and extended dynamically. + +### Strong Defaults with Flexibility + +Graphs in this library are directed by default, allowing for undirected (bidirectional) edges to be layered as transformations. +Sensible algorithm defaults are provided, so users can easily get started without needing to specify algorithms manually. +However, users retain the flexibility to override defaults with specific solutions when required. + +### Extensible Algorithms + +Algorithms in the library are built as abstract definitions with several built-in defaults. +However, the architecture allows users to plug in their own algorithms if needed. +For example, while Kruskal's and Prim's algorithms are provided for finding minimum spanning trees, users could implement their own reverse-delete algorithm, maintaining compatibility with the same API. + +### Flexible Graph Declarations + +The foundation of the library is the `GraphProtocol`, which requires only one method: defining the outgoing edges from a node. +This minimalist design enables a wide range of algorithms – such as traversals, shortest paths, and minimum spanning trees – without requiring a complex interface. + +Multiple concrete graph types conform to this protocol, including eager representations (`Graph`, `BinaryGraph`) and optimized lazy evaluations (`LazyGraph`, `LazyBinaryGraph`). +Specialized types like `WeightedGraph` manage weighted edges, while `GridGraph` provides a convenient structure for 2D grid-based graphs. + +## Contributions + +While the core library provides a solid foundation, certain areas are still under development and not fully production-tested. +Contributions, suggestions, and feedback are highly encouraged. +Feel free to submit issues, start discussions for feature requests, and contribute code via pull requests. diff --git a/Sources/Graph/BinaryGraphProtocol.swift b/Sources/Graph/BinaryGraphProtocol.swift new file mode 100644 index 0000000..3bad89d --- /dev/null +++ b/Sources/Graph/BinaryGraphProtocol.swift @@ -0,0 +1,49 @@ +/// A protocol that defines the requirements for a binary graph structure. +public protocol BinaryGraphProtocol: GraphProtocol { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. Defaults to `Void`. + associatedtype Edge = Void + + /// Returns the binary edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: A `BinaryGraphEdges` instance containing the edges from the specified node. + @inlinable func edges(from node: Node) -> BinaryGraphEdges +} + +/// A structure representing the edges of a node in a binary graph. +public struct BinaryGraphEdges: Container { + /// The type of elements contained in the container. + public typealias Element = GraphEdge + + /// The source node of the edges. + public var source: Node + /// The left-hand side edge. + public var lhs: Element? + /// The right-hand side edge. + public var rhs: Element? + + /// Initializes a new `BinaryGraphEdges` instance with the given source node and edges. + /// - Parameters: + /// - source: The source node of the edges. + /// - lhs: The left-hand side edge. + /// - rhs: The right-hand side edge. + @inlinable public init(source: Node, lhs: Element?, rhs: Element?) { + self.source = source + self.lhs = lhs + self.rhs = rhs + } + + /// An array of elements contained in the container. + @inlinable public var elements: [Element] { [lhs, rhs].compactMap(\.self) } +} + +extension GraphProtocol where Self: BinaryGraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + let edges: BinaryGraphEdges = self.edges(from: node) + return edges.elements + } +} diff --git a/Sources/Graph/BipartiteGraphProtocol.swift b/Sources/Graph/BipartiteGraphProtocol.swift new file mode 100644 index 0000000..027d5e5 --- /dev/null +++ b/Sources/Graph/BipartiteGraphProtocol.swift @@ -0,0 +1,7 @@ +/// A protocol for bipartite graphs. +public protocol BipartiteGraphProtocol: GraphProtocol where Node: Hashable { + /// The left partition of the bipartite graph. + var leftPartition: Set { get } + /// The right partition of the bipartite graph. + var rightPartition: Set { get } +} diff --git a/Sources/Graph/Coloring/Graph+Coloring+Greedy.swift b/Sources/Graph/Coloring/Graph+Coloring+Greedy.swift new file mode 100644 index 0000000..e5165e8 --- /dev/null +++ b/Sources/Graph/Coloring/Graph+Coloring+Greedy.swift @@ -0,0 +1,42 @@ +extension GraphColoringAlgorithm { + /// Creates a new instance of the greedy algorithm. + @inlinable public static func greedy() -> Self where Self == GreedyColoringAlgorithm { + GreedyColoringAlgorithm() + } +} + +extension WholeGraphProtocol where Node: Hashable { + /// Colors the nodes of the graph using the greedy algorithm. + @inlinable public func colorNodes() -> [Node: Int] { + colorNodes(using: .greedy()) + } +} + +/// An implementation of the greedy graph coloring algorithm. +public struct GreedyColoringAlgorithm: GraphColoringAlgorithm { + /// Creates a new instance of the greedy algorithm. + @inlinable public init() {} + + /// Colors the nodes of the graph using the greedy algorithm. + @inlinable public func coloring( + of graph: some WholeGraphProtocol + ) -> [Node: Int] { + var result: [Node: Int] = [:] + let nodes = graph.allNodes.sorted { graph.edges(from: $0).count > graph.edges(from: $1).count } + for node in nodes { + var usedColors: Set = [] + for edge in graph.edges(from: node) { + if let color = result[edge.destination] { + usedColors.insert(color) + } + } + // Find the smallest available color + var color = 0 + while usedColors.contains(color) { + color += 1 + } + result[node] = color + } + return result + } +} diff --git a/Sources/Graph/Coloring/Graph+Coloring.swift b/Sources/Graph/Coloring/Graph+Coloring.swift new file mode 100644 index 0000000..e0cd44e --- /dev/null +++ b/Sources/Graph/Coloring/Graph+Coloring.swift @@ -0,0 +1,25 @@ +extension WholeGraphProtocol where Node: Hashable { + /// Colors the nodes of the graph using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for coloring the nodes. + /// - Returns: A dictionary where the keys are the nodes and the values are the colors assigned to the nodes. + @inlinable public func colorNodes( + using algorithm: some GraphColoringAlgorithm + ) -> [Node: Int] { + algorithm.coloring(of: self) + } +} + +/// A protocol that defines the requirements for a graph coloring algorithm. +public protocol GraphColoringAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Colors the nodes of the graph. + /// - Parameter graph: The graph in which to color the nodes. + /// - Returns: A dictionary where the keys are the nodes and the values are the colors assigned to the nodes. + @inlinable func coloring( + of graph: some WholeGraphProtocol + ) -> [Node: Int] +} diff --git a/Sources/Graph/Concrete graphs/BinaryGraph.swift b/Sources/Graph/Concrete graphs/BinaryGraph.swift new file mode 100644 index 0000000..c16fac6 --- /dev/null +++ b/Sources/Graph/Concrete graphs/BinaryGraph.swift @@ -0,0 +1,71 @@ +/// A binary graph structure that holds nodes and edges. +public struct BinaryGraph { + /// The edges of the binary graph. + @usableFromInline let _edges: [BinaryGraphEdges] + + /// A closure to determine if two nodes are equal. + @usableFromInline let isEqual: (Node, Node) -> Bool + + /// Initializes a new binary graph with the given edges and equality function. + /// - Parameters: + /// - edges: An array of `BinaryGraphEdges` representing the edges of the graph. + /// - isEqual: A closure that takes two nodes and returns a Boolean value indicating whether they are equal. + @inlinable public init(edges: [BinaryGraphEdges], isEqual: @escaping (Node, Node) -> Bool) { + self._edges = edges + self.isEqual = isEqual + } +} + +extension BinaryGraph: BinaryGraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: A `BinaryGraphEdges` instance containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> BinaryGraphEdges { + _edges.first { isEqual($0.source, node) } ?? BinaryGraphEdges(source: node, lhs: nil, rhs: nil) + } +} + +extension BinaryGraph: WholeGraphProtocol where Node: Hashable { + /// All nodes in the binary graph. + public var allNodes: [Node] { + var nodes = Set() + for edge in _edges { + nodes.insert(edge.source) + if let node = edge.lhs?.destination { + nodes.insert(node) + } + if let node = edge.rhs?.destination { + nodes.insert(node) + } + } + return Array(nodes) + } + + /// All edges in the binary graph. + public var allEdges: [GraphEdge] { + _edges.compactMap(\.lhs) + _edges.compactMap(\.rhs) + } +} + +extension BinaryGraph where Node: Equatable { + /// Initializes a new binary graph with the given edges. + /// - Parameter edges: An array of `BinaryGraphEdges` representing the edges of the graph. + @inlinable public init(edges: [BinaryGraphEdges]) { + self.init(edges: edges, isEqual: ==) + } + + /// Initializes a new binary graph with the given edges. + /// - Parameter edges: A dictionary where the key is a node and the value is a tuple containing the left-hand side and right-hand side nodes. + @inlinable public init(edges: [Node: (lhs: Node?, rhs: Node?)?]) where Edge == Void { + self.init( + edges: edges.map { source, destinations in + BinaryGraphEdges( + source: source, + lhs: destinations?.lhs.map { GraphEdge(source: source, destination: $0) }, + rhs: destinations?.rhs.map { GraphEdge(source: source, destination: $0) } + ) + }, + isEqual: == + ) + } +} diff --git a/Sources/Graph/Concrete graphs/Graph.swift b/Sources/Graph/Concrete graphs/Graph.swift new file mode 100644 index 0000000..ed383b0 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Graph.swift @@ -0,0 +1,68 @@ +/// A generic graph structure that holds nodes and edges. +public struct Graph { + /// The edges of the graph. + @usableFromInline let _edges: [GraphEdge] + + /// A closure to determine if two nodes are equal. + @usableFromInline let isEqual: (Node, Node) -> Bool + + /// Initializes a new graph with the given edges and equality function. + /// - Parameters: + /// - edges: An array of `GraphEdge` representing the edges of the graph. + /// - isEqual: A closure that takes two nodes and returns a Boolean value indicating whether they are equal. + @inlinable public init(edges: [GraphEdge], isEqual: @escaping (Node, Node) -> Bool) { + self._edges = edges + self.isEqual = isEqual + } +} + +extension Graph: GraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + self._edges.filter { isEqual($0.source, node) } + } +} + +extension Graph: WholeGraphProtocol where Node: Hashable { + /// All nodes in the graph. + public var allNodes: [Node] { + var nodes = Set() + for edge in _edges { + nodes.insert(edge.source) + nodes.insert(edge.destination) + } + return Array(nodes) + } + + /// All edges in the graph. + public var allEdges: [GraphEdge] { + _edges + } +} + +extension Graph where Node: Equatable { + /// Initializes a new graph with the given edges. + /// - Parameter edges: An array of `GraphEdge` representing the edges of the graph. + @inlinable public init(edges: [GraphEdge]) { + self.init(edges: edges, isEqual: ==) + } + + /// Initializes a new graph with the given edges. + /// - Parameter edges: A dictionary where the key is a node and the value is an array of destination nodes. + @inlinable public init(edges: [Node: [Node]]) where Edge == Void { + self.init(edges: edges.flatMap { source, destinations in + destinations.map { GraphEdge(source: source, destination: $0) } + }, isEqual: ==) + } + + /// Initializes a new graph with the given edges and weights. + /// - Parameter edges: A dictionary where the key is a node and the value is another dictionary + /// where the key is a destination node and the value is the edge weight. + @inlinable public init(edges: [Node: [Node: Edge]]) where Node: Hashable { + self.init(edges: edges.flatMap { source, destinations in + destinations.map { GraphEdge(source: source, destination: $0, value: $1) } + }, isEqual: ==) + } +} diff --git a/Sources/Graph/Concrete graphs/GridGraph.swift b/Sources/Graph/Concrete graphs/GridGraph.swift new file mode 100644 index 0000000..5b83810 --- /dev/null +++ b/Sources/Graph/Concrete graphs/GridGraph.swift @@ -0,0 +1,224 @@ +import Collections + +/// A graph structure representing a grid where each cell holds a value and edges can be defined between nodes. +public struct GridGraph { + /// A node in the grid graph, representing a cell with a position + public typealias Node = GridPosition + + /// The grid of values representing the graph. + public let grid: [[Value]] + /// The set of available directions for edges in the grid. + public let availableDirections: OrderedSet + /// A closure to determine the edge between two nodes. + @usableFromInline let edge: (Node, Node) -> Edge + + /// Initializes a new grid graph with the given grid, available directions, and edge function. + /// - Parameters: + /// - grid: A 2D array representing the grid of values. + /// - availableDirections: The set of directions in which edges can be created. Defaults to orthogonal and diagonal directions. + /// - edge: A closure that takes two nodes and returns the edge between them. + @inlinable public init( + grid: [some Sequence], + availableDirections: OrderedSet = .orthogonal + .diagonal, + edge: @escaping (Node, Node) -> Edge + ) { + self.grid = grid.map(Array.init) + self.availableDirections = availableDirections + self.edge = edge + } + + /// Initializes a new grid graph with the given grid and available directions, assuming edges have no value. + /// - Parameters: + /// - grid: A 2D array representing the grid of values. + /// - availableDirections: The set of directions in which edges can be created. Defaults to orthogonal and diagonal directions. + @inlinable public init( + grid: [some Sequence], + availableDirections: OrderedSet = .orthogonal + .diagonal + ) where Edge == Void { + self.init(grid: grid, availableDirections: availableDirections) { _, _ in () } + } + + /// Accesses the value at the specified position in the grid. + /// - Parameter position: The position in the grid. + /// - Returns: The value at the specified position. + @inlinable public subscript(position: GridPosition) -> Value { + grid[position] + } +} + +extension GridGraph: GraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances representing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + grid.neighbors(of: node, in: availableDirections).map { + GraphEdge(source: node, destination: $0, value: edge(node, $0)) + } + } +} + +extension GridGraph: WholeGraphProtocol { + /// Returns all nodes in the grid. + public var allNodes: [Node] { + grid.positions.compactMap(\.self) + } +} + +/// Represents a position in the grid with x and y coordinates. +public struct GridPosition: Hashable { + /// The x-coordinate of the position. + public let x: Int + /// The y-coordinate of the position. + public let y: Int + + /// Initializes a new `GridPosition` with the given coordinates. + /// - Parameters: + /// - x: The x-coordinate. + /// - y: The y-coordinate. + @inlinable public init(x: Int, y: Int) { + self.x = x + self.y = y + } + + /// Moves the position by the given movement. + /// - Parameter movement: The movement to apply. + /// - Returns: A new `GridPosition` after applying the movement. + @inlinable public func move(_ movement: GridMovement) -> Self { + GridPosition( + x: x + movement.x, + y: y + movement.y + ) + } + + /// The coordinates of the position as a SIMD2 vector. + @inlinable public var coordinates: SIMD2 { + SIMD2(Double(x), Double(y)) + } +} + +/// Represents movement in a 2D grid. +public struct GridMovement: Hashable { + /// The x offset of the movement. + public let x: Int + /// The y offset of the movement. + public let y: Int + + /// Initializes a new `GridMovement` with the given offsets. + /// - Parameters: + /// - x: The x-offset. + /// - y: The y-offset. + @inlinable public init(x: Int, y: Int) { + self.x = x + self.y = y + } + + /// Repeats the movement a given number of times. + /// - Parameter count: The number of times to repeat the movement. + /// - Returns: A new `GridMovement` with the repeated offsets. + @inlinable public func repeated(times count: Int) -> GridMovement { + GridMovement( + x: x * count, + y: y * count + ) + } +} + +/// Represents a direction in the 2D space. +public enum GridDirection: CaseIterable, Hashable, Sendable { + case up, right, down, left + case upRight, downRight, downLeft, upLeft + + /// The movement associated with the direction. + @inlinable public var movement: GridMovement { + switch self { + case .up: GridMovement(x: 0, y: -1) + case .down: GridMovement(x: 0, y: 1) + case .left: GridMovement(x: -1, y: 0) + case .right: GridMovement(x: 1, y: 0) + case .upLeft: GridMovement(x: -1, y: -1) + case .upRight: GridMovement(x: 1, y: -1) + case .downLeft: GridMovement(x: -1, y: 1) + case .downRight: GridMovement(x: 1, y: 1) + } + } +} + +extension OrderedSet { + /// A set of horizontal directions (left and right). + public static let horizontal: Self = [.left, .right] + + /// A set of vertical directions (up and down). + public static let vertical: Self = [.up, .down] + + /// A set of orthogonal directions (right, down, left, up). + public static let orthogonal: Self = [.right, .down, .left, .up] + + /// A set of diagonal directions (up-right, down-right, down-left, up-left). + public static let diagonal: Self = [.upRight, .downRight, .downLeft, .upLeft] + + /// Combines two sets of directions. + /// - Parameters: + /// - lhs: The first set of directions. + /// - rhs: The second set of directions. + /// - Returns: A new set containing all directions from both sets. + public static func + (lhs: Self, rhs: Self) -> Self { + lhs.union(rhs) + } + + /// Subtracts one set of directions from another. + /// - Parameters: + /// - lhs: The first set of directions. + /// - rhs: The second set of directions. + /// - Returns: A new set containing the directions from the first set that are not in the second set. + public static func - (lhs: Self, rhs: Self) -> Self { + lhs.subtracting(rhs) + } +} + +extension Array where Element: Collection, Element.Index == Int { + /// Returns a sequence of all positions in the grid. + @inlinable public var positions: some Sequence { + self.lazy.enumerated().flatMap { x, line in + line.indices.lazy.map { GridPosition(x: x, y: $0) } + } + } + + /// Accesses the element at the specified grid position. + /// - Parameter position: The position in the grid. + /// - Returns: The element at the specified position. + @inlinable public subscript(position: GridPosition) -> Element.Element { + self[position.y][position.x] + } + + /// Accesses the element at the specified grid position, wrapping around if the position is out of bounds. + /// - Parameter position: The position in the grid. + /// - Returns: The element at the specified position, wrapping around if necessary. + @inlinable public subscript(infinite position: GridPosition) -> Element.Element { + let row = nonNegativeModulo(of: position.y, by: count) + let column = nonNegativeModulo(of: position.x, by: self[row].count) + return self[row][column] + } + + /// Accesses the element at the specified grid position, returning nil if the position is out of bounds. + /// - Parameter position: The position in the grid. + /// - Returns: The element at the specified position, or nil if the position is out of bounds. + @inlinable public subscript(safe position: GridPosition) -> Element.Element? { + self[safe: position.y]?[safe: position.x] + } + + /// Returns the neighboring positions of the specified grid position in the given directions. + /// - Parameters: + /// - position: The position in the grid. + /// - directions: The directions in which to find neighbors. + /// - Returns: A sequence of neighboring positions. + @inlinable public func neighbors(of position: GridPosition, in directions: some Sequence) -> some Sequence { + directions.lazy.map(\.movement).map(position.move).filter(contains) + } + + /// Checks if the grid contains the specified position. + /// - Parameter position: The position to check. + /// - Returns: True if the grid contains the position, false otherwise. + @inlinable public func contains(position: GridPosition) -> Bool { + indices.contains(position.y) && self[position.y].indices.contains(position.x) + } +} diff --git a/Sources/Graph/Concrete graphs/HashedBinaryGraph.swift b/Sources/Graph/Concrete graphs/HashedBinaryGraph.swift new file mode 100644 index 0000000..06d7b21 --- /dev/null +++ b/Sources/Graph/Concrete graphs/HashedBinaryGraph.swift @@ -0,0 +1,48 @@ +/// A binary graph structure that uses hashed values for nodes to efficiently store and retrieve edges. +public struct HashedBinaryGraph { + /// A dictionary mapping hashed values to the edges of the binary graph. + @usableFromInline let _edges: [HashValue: BinaryGraphEdges] + /// A closure to compute the hash value of a node. + @usableFromInline let hashValue: (Node) -> HashValue + + /// Initializes a new hashed binary graph with the given edges and hash function. + /// - Parameters: + /// - edges: An array of `BinaryGraphEdges` representing the edges of the graph. + /// - hashValue: A closure that takes a node and returns its hash value. + public init(edges: [BinaryGraphEdges], hashValue: @escaping (Node) -> HashValue) { + self._edges = edges.keyed { hashValue($0.source) } + self.hashValue = hashValue + } +} + +extension HashedBinaryGraph: BinaryGraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: A `BinaryGraphEdges` instance containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> BinaryGraphEdges { + _edges[hashValue(node)] ?? BinaryGraphEdges(source: node, lhs: nil, rhs: nil) + } +} + +extension HashedBinaryGraph where Node: Hashable, HashValue == Node { + /// Initializes a new hashed binary graph with the given edges. + /// - Parameter edges: An array of `BinaryGraphEdges` representing the edges of the graph. + @inlinable public init(edges: [BinaryGraphEdges]) { + self.init(edges: edges, hashValue: \.self) + } + + /// Initializes a new hashed binary graph with the given edges. + /// - Parameter edges: A dictionary where the key is a node and the value is a tuple containing the left-hand side and right-hand side nodes. + @inlinable public init(edges: [Node: (lhs: Node?, rhs: Node?)?]) where Node: Hashable, Edge == Void { + self.init( + edges: edges.map { source, destinations in + BinaryGraphEdges( + source: source, + lhs: destinations?.lhs.map { GraphEdge(source: source, destination: $0) }, + rhs: destinations?.rhs.map { GraphEdge(source: source, destination: $0) } + ) + }, + hashValue: \.self + ) + } +} diff --git a/Sources/Graph/Concrete graphs/HashedGraph.swift b/Sources/Graph/Concrete graphs/HashedGraph.swift new file mode 100644 index 0000000..acbd9a9 --- /dev/null +++ b/Sources/Graph/Concrete graphs/HashedGraph.swift @@ -0,0 +1,69 @@ +import Algorithms + +/// A graph structure that uses hashed values for nodes to efficiently store and retrieve edges. +public struct HashedGraph { + /// A dictionary mapping hashed values to the edges of the graph. + @usableFromInline let _edges: [HashValue: [GraphEdge]] + /// A closure to compute the hash value of a node. + @usableFromInline let hashValue: (Node) -> HashValue + + /// Initializes a new hashed graph with the given edges and hash function. + /// - Parameters: + /// - edges: A dictionary mapping hashed values to arrays of `GraphEdge` instances. + /// - hashValue: A closure that takes a node and returns its hash value. + @inlinable public init(edges: [HashValue: [GraphEdge]], hashValue: @escaping (Node) -> HashValue) { + self._edges = edges + self.hashValue = hashValue + } + + /// Initializes a new hashed graph with the given edges and hash function. + /// - Parameters: + /// - edges: An array of `GraphEdge` instances. + /// - hashValue: A closure that takes a node and returns its hash value. + @inlinable public init(edges: [GraphEdge], hashValue: @escaping (Node) -> HashValue) { + self._edges = edges.grouped { hashValue($0.source) } + self.hashValue = hashValue + } +} + +extension HashedGraph: GraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + self._edges[hashValue(node)] ?? [] + } +} + +extension HashedGraph: WholeGraphProtocol { + /// All nodes in the graph. + public var allNodes: [Node] { + var map: [HashValue: Node] = [:] + for edge in allEdges { + map[hashValue(edge.source)] = edge.source + map[hashValue(edge.destination)] = edge.destination + } + return Array(map.values) + } + + /// All edges in the graph. + public var allEdges: [GraphEdge] { + _edges.values.flatMap(\.self) + } +} + +extension HashedGraph where Node: Hashable, HashValue == Node { + /// Initializes a new hashed graph with the given edges. + /// - Parameter edges: An array of `GraphEdge` instances. + @inlinable public init(edges: [GraphEdge]) { + self.init(edges: edges, hashValue: \.self) + } + + /// Initializes a new hashed graph with the given edges. + /// - Parameter edges: A dictionary mapping nodes to arrays of destination nodes. + @inlinable public init(edges: [Node: [Node]]) where Edge == Void { + self.init(edges: edges.flatMap { source, destinations in + destinations.map { GraphEdge(source: source, destination: $0) } + }.grouped(by: \.source), hashValue: \.self) + } +} diff --git a/Sources/Graph/Concrete graphs/LazyBinaryGraph.swift b/Sources/Graph/Concrete graphs/LazyBinaryGraph.swift new file mode 100644 index 0000000..5f697fd --- /dev/null +++ b/Sources/Graph/Concrete graphs/LazyBinaryGraph.swift @@ -0,0 +1,34 @@ +/// A lazy binary graph structure that computes edges on demand. +public struct LazyBinaryGraph { + /// A closure that returns the edges for a given node. + @usableFromInline let _edges: (Node) -> BinaryGraphEdges + + /// Initializes a new lazy binary graph with a closure that provides neighbor nodes. + /// - Parameter neighborNodes: A closure that takes a node and returns an optional tuple containing the left-hand side and right-hand side neighbor nodes. + @inlinable public init(neighborNodes: @escaping (Node) -> (lhs: Node?, rhs: Node?)?) where Edge == Void { + _edges = { node in + let destinations = neighborNodes(node) + return BinaryGraphEdges( + source: node, + lhs: destinations?.lhs.map { .init(source: node, destination: $0) }, + rhs: destinations?.rhs.map { .init(source: node, destination: $0) } + ) + } + } + + /// Initializes a new lazy binary graph with a custom edges closure. + /// - Parameter edges: A closure that takes a node and returns its `BinaryGraphEdges`. + @_disfavoredOverload + @inlinable public init(customEdges edges: @escaping (Node) -> BinaryGraphEdges) { + _edges = edges + } +} + +extension LazyBinaryGraph: BinaryGraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: A `BinaryGraphEdges` instance containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> BinaryGraphEdges { + _edges(node) + } +} diff --git a/Sources/Graph/Concrete graphs/LazyGraph.swift b/Sources/Graph/Concrete graphs/LazyGraph.swift new file mode 100644 index 0000000..9dda738 --- /dev/null +++ b/Sources/Graph/Concrete graphs/LazyGraph.swift @@ -0,0 +1,71 @@ +/// A lazy graph structure that computes edges on demand. +public struct LazyGraph { + /// A closure that returns the edges for a given node. + @usableFromInline let _edges: (Node) -> [GraphEdge] + + /// Initializes a new lazy graph with a closure that provides neighbor nodes. + /// - Parameter neighborNodes: A closure that takes a node and returns a sequence of neighbor nodes. + @inlinable public init>(neighborNodes: @escaping (Node) -> Nodes) where Edge == Void { + _edges = { node in neighborNodes(node).lazy.map { GraphEdge(source: node, destination: $0) } } + } + + /// Initializes a new lazy graph with a closure that provides a single neighbor node. + /// - Parameter neighbor: A closure that takes a node and returns its single neighbor node. + @inlinable public init(neighbor: @escaping (Node) -> Node) where Edge == Void { + self.init(neighborNodes: { CollectionOfOne(neighbor($0)) }) + } + + /// Initializes a new lazy graph with a custom edges closure. + /// - Parameter edges: A closure that takes a node and returns an array of `GraphEdge` instances. + @_disfavoredOverload + @inlinable public init(customEdges edges: @escaping (Node) -> [GraphEdge]) { + _edges = edges + } + + /// Materializes the lazy graph into a concrete `Graph` starting from the specified node using the given traversal strategy. + /// - Parameters: + /// - starting: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - isEqual: A closure that takes two nodes and returns a Boolean value indicating whether they are equal. + /// - Returns: A concrete `Graph` instance. + @inlinable public func materialize(starting: Node, strategy: some GraphTraversalStrategy, isEqual: @escaping (Node, Node) -> Bool) -> Graph { + Graph(edges: traverse(from: starting, strategy: strategy).flatMap(edges), isEqual: isEqual) + } + + /// Materializes the lazy graph into a concrete `Graph` starting from the specified node using the given traversal strategy. + /// - Parameters: + /// - starting: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: A concrete `Graph` instance. + @inlinable public func materialize(starting: Node, strategy: some GraphTraversalStrategy) -> Graph where Node: Equatable { + Graph(edges: traverse(from: starting, strategy: strategy).flatMap(edges)) + } + + /// Materializes the lazy graph into a concrete `HashedGraph` starting from the specified node using the given traversal strategy. + /// - Parameters: + /// - starting: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - hashValue: A closure that takes a node and returns its hash value. + /// - Returns: A concrete `HashedGraph` instance. + @inlinable public func materialize(starting: Node, strategy: some GraphTraversalStrategy, hashValue: @escaping (Node) -> HashValue) -> HashedGraph { + HashedGraph(edges: traverse(from: starting, strategy: strategy).flatMap(edges), hashValue: hashValue) + } + + /// Materializes the lazy graph into a concrete `HashedGraph` starting from the specified node using the given traversal strategy. + /// - Parameters: + /// - starting: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: A concrete `HashedGraph` instance. + @inlinable public func materialize(starting: Node, strategy: some GraphTraversalStrategy) -> HashedGraph where Node: Hashable { + HashedGraph(edges: traverse(from: starting, strategy: strategy).flatMap(edges)) + } +} + +extension LazyGraph: GraphProtocol { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + _edges(node) + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/BipartiteGraph.swift b/Sources/Graph/Concrete graphs/Transformations/BipartiteGraph.swift new file mode 100644 index 0000000..77fc613 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/BipartiteGraph.swift @@ -0,0 +1,49 @@ +/// A bipartite graph. +public struct BipartiteGraph: BipartiteGraphProtocol where Base.Node: Hashable { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + + /// The base graph. + public let base: Base + /// The left partition of the bipartite graph. + public let leftPartition: Set + /// The right partition of the bipartite graph. + public let rightPartition: Set + + /// Initializes a new `BipartiteGraph` with a base graph and the left and right partitions. + /// - Parameters: + /// - base: The base graph. + /// - leftPartition: The left partition of the bipartite graph. + /// - rightPartition: The right partition of the bipartite graph. + /// - Returns: A `BipartiteGraph` instance. + @inlinable public init( + base: Base, + leftPartition: Set, + rightPartition: Set + ) { + self.base = base + self.leftPartition = leftPartition + self.rightPartition = rightPartition + } + + /// Returns the edges from a given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + guard leftPartition.contains(node) else { + return [] + } + return base.edges(from: node).filter { edge in + rightPartition.contains(edge.destination) + } + } +} + +extension GraphProtocol where Node: Hashable { + /// Returns a bipartite graph with the given left and right partitions. + /// - Parameters: + /// - leftPartition: The left partition of the bipartite graph. + /// - rightPartition: The right partition of the bipartite graph. + /// - Returns: A `BipartiteGraph` instance. + @inlinable public func bipartite(leftPartition: Set, rightPartition: Set) -> BipartiteGraph { + .init(base: self, leftPartition: leftPartition, rightPartition: rightPartition) + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/ComplementGraph.swift b/Sources/Graph/Concrete graphs/Transformations/ComplementGraph.swift new file mode 100644 index 0000000..be9b3fc --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/ComplementGraph.swift @@ -0,0 +1,37 @@ +/// A graph structure that represents the complement of a given base graph. +@dynamicMemberLookup +public struct ComplementGraph: GraphProtocol where Base.Node: Hashable { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + + /// The base graph from which the complement is derived. + @usableFromInline let base: Base + /// The default value for edges in the complement graph. + @usableFromInline let defaultEdgeValue: Edge + + /// Initializes a new complement graph with the given base graph and default edge value. + /// - Parameters: + /// - base: The base graph from which the complement is derived. + /// - defaultEdgeValue: The default value for edges in the complement graph. + @inlinable public init(base: Base, defaultEdgeValue: Edge) { + self.base = base + self.defaultEdgeValue = defaultEdgeValue + } + + /// Returns the edges originating from the specified node in the complement graph. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node in the complement graph. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + let existingDestinations = Set(base.edges(from: node).map(\.destination)) + let allNodes = base.allNodes.filter { $0 != node } + let complementDestinations = allNodes.filter { !existingDestinations.contains($0) } + return complementDestinations.map { + GraphEdge(source: node, destination: $0, value: defaultEdgeValue) + } + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/Graph+Equatable.swift b/Sources/Graph/Concrete graphs/Transformations/Graph+Equatable.swift new file mode 100644 index 0000000..8840e86 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/Graph+Equatable.swift @@ -0,0 +1,141 @@ +/// A graph wrapper that makes the nodes equatable based on a custom equality function. +@dynamicMemberLookup +public struct EquatableNodesGraph: GraphProtocol { + public typealias Edge = Base.Edge + + /// A node in the `EquatableNodesGraph` that is equatable based on a custom equality function. + public struct Node: Equatable { + /// The base node. + @usableFromInline let base: Base.Node + /// The custom equality function for the node. + @usableFromInline let isEqual: (Base.Node, Base.Node) -> Bool + + /// Initializes a new node with a base node and a custom equality function. + /// - Parameters: + /// - base: The base node. + /// - isEqual: A closure that defines the equality function for the nodes. + @inlinable public init(base: Base.Node, isEqual: @escaping (Base.Node, Base.Node) -> Bool) { + self.base = base + self.isEqual = isEqual + } + + /// Checks if two nodes are equal based on the custom equality function. + /// - Parameters: + /// - lhs: The left-hand side node. + /// - rhs: The right-hand side node. + /// - Returns: `true` if the nodes are equal, `false` otherwise. + @inlinable public static func == (lhs: Node, rhs: Node) -> Bool { + lhs.isEqual(lhs.base, rhs.base) + } + } + + /// The base graph. + @usableFromInline let base: Base + /// The custom equality function for the nodes. + @usableFromInline let isEqual: (Base.Node, Base.Node) -> Bool + + /// Initializes a new `EquatableNodesGraph` with a base graph and a custom equality function for the nodes. + /// - Parameters: + /// - base: The base graph. + /// - isEqual: A closure that defines the equality function for the nodes. + @inlinable public init(base: Base, isEqual: @escaping (Base.Node, Base.Node) -> Bool) { + self.base = base + self.isEqual = isEqual + } + + /// Returns the edges from a given node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of edges from the given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + base.edges(from: node.base).map { + $0.mapNode { + Node(base: $0, isEqual: isEqual) + } + } + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} + +extension GraphProtocol { + /// Creates a new `EquatableNodesGraph` with a custom equality function for the nodes. + /// - Parameter isEqual: A closure that defines the equality function for the nodes. + /// - Returns: An `EquatableNodesGraph` instance. + @inlinable public func makeNodesEquatable(by isEqual: @escaping (Node, Node) -> Bool) -> EquatableNodesGraph { + .init(base: self, isEqual: isEqual) + } +} + +/// A graph wrapper that makes the edges equatable based on a custom equality function. +@dynamicMemberLookup +public struct EquatableEdgesGraph: GraphProtocol { + public typealias Node = Base.Node + + /// An edge in the `EquatableEdgesGraph` that is equatable based on a custom equality function. + public struct Edge: Equatable { + /// The base edge. + @usableFromInline let base: Base.Edge + /// The custom equality function for the edge. + @usableFromInline let isEqual: (Base.Edge, Base.Edge) -> Bool + + /// Initializes a new edge with a base edge and a custom equality function. + /// - Parameters: + /// - base: The base edge. + /// - isEqual: A closure that defines the equality function for the edges. + @inlinable public init(base: Base.Edge, isEqual: @escaping (Base.Edge, Base.Edge) -> Bool) { + self.base = base + self.isEqual = isEqual + } + + /// Checks if two edges are equal based on the custom equality function. + /// - Parameters: + /// - lhs: The left-hand side edge. + /// - rhs: The right-hand side edge. + /// - Returns: `true` if the edges are equal, `false` otherwise. + @inlinable public static func == (lhs: Edge, rhs: Edge) -> Bool { + lhs.isEqual(lhs.base, rhs.base) + } + } + + /// The base graph. + @usableFromInline let base: Base + /// The custom equality function for the edges. + @usableFromInline let isEqual: (Base.Edge, Base.Edge) -> Bool + + /// Initializes a new `EquatableEdgesGraph` with a base graph and a custom equality function for the edges. + /// - Parameters: + /// - base: The base graph. + /// - isEqual: A closure that defines the equality function for the edges. + @inlinable public init(base: Base, isEqual: @escaping (Base.Edge, Base.Edge) -> Bool) { + self.base = base + self.isEqual = isEqual + } + + /// Returns the edges from a given node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of edges from the given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + base.edges(from: node).map { + $0.mapEdge { + Edge(base: $0, isEqual: isEqual) + } + } + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} + +extension GraphProtocol { + /// Creates a new `EquatableEdgesGraph` with a custom equality function for the edges. + /// - Parameter isEqual: A closure that defines the equality function for the edges. + /// - Returns: An `EquatableEdgesGraph` instance. + @inlinable public func makeEdgesEquatable(by isEqual: @escaping (Edge, Edge) -> Bool) -> EquatableEdgesGraph { + .init(base: self, isEqual: isEqual) + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/Graph+Hashable.swift b/Sources/Graph/Concrete graphs/Transformations/Graph+Hashable.swift new file mode 100644 index 0000000..51ba5de --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/Graph+Hashable.swift @@ -0,0 +1,135 @@ +/// A graph wrapper that makes the nodes hashable based on a custom hash value. +@dynamicMemberLookup +public struct HashableNodesGraph: GraphProtocol where Base.Node: Equatable { + public typealias Edge = Base.Edge + + /// A node in the `HashableNodesGraph` that is hashable based on a custom hash value. + public struct Node: Hashable { + /// The base node. + @usableFromInline let base: Base.Node + /// The custom hash value for the node. + @usableFromInline let hashValue: HashValue + + /// Initializes a new node with a base node and a custom hash value. + /// - Parameters: + /// - base: The base node. + /// - hashValue: The custom hash value for the node. + @inlinable public init(base: Base.Node, hashValue: HashValue) { + self.base = base + self.hashValue = hashValue + } + + /// Hashes the essential components of the node by combining the custom hash value. + /// - Parameter hasher: The hasher to use when combining the components of the node. + @inlinable public func hash(into hasher: inout Hasher) { + hasher.combine(hashValue) + } + } + + /// A graph wrapper that makes the nodes hashable based on a custom hash value. + @usableFromInline let base: Base + /// A closure that defines the hash value function for the nodes. + @usableFromInline let hashValue: (Base.Node) -> HashValue + + /// Initializes a new `HashableNodesGraph` with a base graph and a custom hash value function for the nodes. + /// - Parameters: + /// - base: The base graph. + /// - hashValue: A closure that defines the hash value function for the nodes. + @inlinable public init(base: Base, hashValue: @escaping (Base.Node) -> HashValue) { + self.base = base + self.hashValue = hashValue + } + + /// Returns the edges from a given node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of edges from the given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + base.edges(from: node.base).map { + $0.mapNode { + Node(base: $0, hashValue: hashValue($0)) + } + } + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} + +extension GraphProtocol where Node: Equatable { + /// Creates a new `HashableNodesGraph` with a custom hash value function for the nodes. + /// - Parameter hashValue: A closure that defines the hash value function for the nodes. + /// - Returns: A `HashableNodesGraph` instance. + @inlinable public func makeNodesHashable(by hashValue: @escaping (Node) -> HashValue) -> HashableNodesGraph { + .init(base: self, hashValue: hashValue) + } +} + +/// A graph wrapper that makes the edges hashable based on a custom hash value. +@dynamicMemberLookup +public struct HashableEdgesGraph: GraphProtocol where Base.Edge: Equatable { + public typealias Node = Base.Node + + /// An edge in the `HashableEdgesGraph` that is hashable based on a custom hash value. + public struct Edge: Hashable { + /// The base edge. + @usableFromInline let base: Base.Edge + /// The custom hash value for the edge. + @usableFromInline let hashValue: HashValue + + /// Initializes a new edge with a base edge and a custom hash value. + /// - Parameters: + /// - base: The base edge. + /// - hashValue: The custom hash value for the edge. + @inlinable public init(base: Base.Edge, hashValue: HashValue) { + self.base = base + self.hashValue = hashValue + } + + /// Hashes the essential components of the edge by combining the custom hash value. + /// - Parameter hasher: The hasher to use when combining the components of the edge. + @inlinable public func hash(into hasher: inout Hasher) { + hasher.combine(hashValue) + } + } + + /// A graph wrapper that makes the edges hashable based on a custom hash value. + @usableFromInline let base: Base + /// A closure that defines the hash value function for the edges. + @usableFromInline let hashValue: (Base.Edge) -> HashValue + + /// Initializes a new `HashableEdgesGraph` with a base graph and a custom hash value function for the edges. + /// - Parameters: + /// - base: The base graph. + /// - hashValue: A closure that defines the hash value function for the edges. + @inlinable public init(base: Base, hashValue: @escaping (Base.Edge) -> HashValue) { + self.base = base + self.hashValue = hashValue + } + + /// Returns the edges from a given node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of edges from the given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + base.edges(from: node).map { + $0.mapEdge { + Edge(base: $0, hashValue: hashValue($0)) + } + } + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} + +extension GraphProtocol where Edge: Equatable { + /// Creates a new `HashableEdgesGraph` with a custom hash value function for the edges. + /// - Parameter hashValue: A closure that defines the hash value function for the edges. + /// - Returns: A `HashableEdgesGraph` instance. + @inlinable public func makeEdgesHashable(by hashValue: @escaping (Edge) -> HashValue) -> HashableEdgesGraph { + .init(base: self, hashValue: hashValue) + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/ResidualGraph.swift b/Sources/Graph/Concrete graphs/Transformations/ResidualGraph.swift new file mode 100644 index 0000000..682db53 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/ResidualGraph.swift @@ -0,0 +1,119 @@ +/// A graph that represents the residual capacities of a flow network. +@dynamicMemberLookup +public struct ResidualGraph: GraphProtocol where Graph.Node: Hashable, Graph.Edge: Weighted, Graph.Edge.Weight: Numeric { + public typealias Node = Graph.Node + public typealias Edge = Graph.Edge.Weight + + /// The base graph representing the original flow network. + @usableFromInline var base: Graph + /// A dictionary representing the flow values between nodes. + @usableFromInline var flows: [Graph.Node: [Graph.Node: Edge]] + + /// Initializes a residual graph with a given graph. + /// - Parameter graph: The original graph. + @inlinable public init(base: Graph) { + self.base = base + self.flows = [:] + } + + /// Returns the residual edges from a given node. + /// - Parameter node: The source node. + /// - Returns: An array of residual edges from the given node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + var residualEdges: [GraphEdge] = [] + for edge in base.edges(from: node) { + let capacity = edge.value.weight + let flow = flows[node]?[edge.destination] ?? 0 + let residualCapacity = capacity - flow + if residualCapacity > 0 { + residualEdges.append(GraphEdge(source: node, destination: edge.destination, value: residualCapacity)) + } + + // Add reverse edge for possible flow cancellation + let reverseFlow = flows[edge.destination]?[node] ?? 0 + if reverseFlow > 0 { + residualEdges.append(GraphEdge(source: edge.destination, destination: node, value: reverseFlow)) + } + } + return residualEdges + } + + /// Adds flow along a given path. + /// - Parameters: + /// - path: An array of nodes representing the path. + /// - flow: The flow to be added along the path. + @inlinable public mutating func addFlow(path: [Graph.Node], flow: Edge) { + for i in 0.. Edge { + flows[u]?[v] ?? 0 + } + + /// Returns the capacity of the edge from node `u` to node `v`. + /// - Parameters: + /// - u: The source node. + /// - v: The destination node. + /// - Returns: The capacity of the edge from `u` to `v`. + @inlinable public func capacity(from u: Node, to v: Node) -> Edge { + base.edges(from: u).first(where: { $0.destination == v })?.value.weight ?? 0 + } + + /// Returns the residual capacity of the edge from node `u` to node `v`. + /// - Parameters: + /// - u: The source node. + /// - v: The destination node. + /// - Returns: The residual capacity of the edge from `u` to `v`. + @inlinable public func residualCapacity(from u: Node, to v: Node) -> Edge { + capacity(from: u, to: v) - flow(from: u, to: v) + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} + +extension ResidualGraph { + /// Returns the set of nodes reachable from the source node in the residual graph. + /// - Parameter source: The source node. + /// - Returns: A set of nodes reachable from the source node. + @inlinable public func reachableNodes(from source: Node) -> Set { + var reachable = Set() + var queue: [Node] = [source] + reachable.insert(source) + + while !queue.isEmpty { + let current = queue.removeFirst() + for edge in edges(from: current) { + let neighbor = edge.destination + if !reachable.contains(neighbor) && edge.value > .zero { + reachable.insert(neighbor) + queue.append(neighbor) + } + } + } + + return reachable + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/TransposedGraph.swift b/Sources/Graph/Concrete graphs/Transformations/TransposedGraph.swift new file mode 100644 index 0000000..b991ac8 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/TransposedGraph.swift @@ -0,0 +1,42 @@ +/// A graph structure representing the transpose of a given base graph. +@dynamicMemberLookup +public struct TransposedGraph: WholeGraphProtocol where Base.Node: Hashable { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + + /// The base graph from which the transpose is derived. + @usableFromInline var base: Base + /// An adjacency list representing the transposed edges. + @usableFromInline var adjacencyList: [Node: [GraphEdge]] = [:] + + /// Initializes a new transposed graph with the given base graph. + /// - Parameter base: The base graph from which the transpose is derived. + @inlinable public init(base: Base) { + self.base = base + for edge in base.allEdges { + adjacencyList[edge.destination, default: []].append(edge.reversed) + } + } + + /// Returns the edges originating from the specified node in the transposed graph. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node in the transposed graph. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + return adjacencyList[node] ?? [] + } + + /// All nodes in the transposed graph. + @inlinable public var allNodes: [Base.Node] { + base.allNodes + } + + /// All edges in the transposed graph. + @inlinable public var allEdges: [GraphEdge] { + base.allEdges.map(\.reversed) + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} diff --git a/Sources/Graph/Concrete graphs/Transformations/UndirectedGraph.swift b/Sources/Graph/Concrete graphs/Transformations/UndirectedGraph.swift new file mode 100644 index 0000000..47ba321 --- /dev/null +++ b/Sources/Graph/Concrete graphs/Transformations/UndirectedGraph.swift @@ -0,0 +1,43 @@ +/// A graph structure representing an undirected graph derived from a given base graph. +@dynamicMemberLookup +public struct UndirectedGraph: WholeGraphProtocol where Base.Node: Hashable { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + + /// The base graph from which the undirected graph is derived. + @usableFromInline let base: Base + /// An adjacency list representing the edges in the undirected graph. + @usableFromInline var adjacencyList: [Node: [GraphEdge]] = [:] + + /// Initializes a new undirected graph with the given base graph. + /// - Parameter base: The base graph from which the undirected graph is derived. + @inlinable public init(base: Base) { + self.base = base + for edge in base.allEdges { + adjacencyList[edge.destination, default: []].append(edge) + } + } + + /// Returns the edges originating from the specified node in the undirected graph. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node in the undirected graph. + public func edges(from node: Node) -> [GraphEdge] { + base.edges(from: node) + (adjacencyList[node] ?? []) + } + + /// All edges in the undirected graph. + /// - Note: This includes both the original and reversed edges from the base graph. + public var allEdges: [GraphEdge] { + base.allEdges + base.allEdges.map(\.reversed) + } + + /// All nodes in the undirected graph. + public var allNodes: [Base.Node] { + base.allNodes + } + + /// Subscript that accesses members on the underlying graph instance + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + base[keyPath: keyPath] + } +} diff --git a/Sources/Graph/Concrete graphs/WeightedGraph.swift b/Sources/Graph/Concrete graphs/WeightedGraph.swift new file mode 100644 index 0000000..b4921a4 --- /dev/null +++ b/Sources/Graph/Concrete graphs/WeightedGraph.swift @@ -0,0 +1,100 @@ +/// A graph structure that adds weights to the edges of an existing graph. +@dynamicMemberLookup +public struct WeightedGraph { + public typealias Node = Graph.Node + + /// The underlying graph. + @usableFromInline let graph: Graph + /// A closure to compute the weight of an edge between two nodes. + @usableFromInline let weight: (Node, Node) -> Edge + + /// Initializes a new weighted graph with the given underlying graph and weight function. + /// - Parameters: + /// - graph: The underlying graph. + /// - weight: A closure that takes two nodes and returns the weight of the edge between them. + @inlinable public init( + graph: Graph, + weight: @escaping (Node, Node) -> Edge + ) where Graph.Edge == Void { + self.graph = graph + self.weight = weight + } + + @inlinable public subscript(dynamicMember keyPath: KeyPath) -> Member { + graph[keyPath: keyPath] + } +} + +extension WeightedGraph: GraphProtocol { + /// Returns the edges originating from the specified node, with weights added. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node, with weights added. + public func edges(from node: Graph.Node) -> [GraphEdge] { + graph.edges(from: node).map { + GraphEdge( + source: $0.source, + destination: $0.destination, + value: weight($0.source, $0.destination) + ) + } + } +} + +extension WeightedGraph: WholeGraphProtocol where Graph: WholeGraphProtocol { + /// All nodes in the weighted graph. + public var allNodes: [Node] { + graph.allNodes + } + + /// All edges in the weighted graph, with weights added. + public var allEdges: [GraphEdge] { + graph.allEdges.map { edge in + GraphEdge( + source: edge.source, + destination: edge.destination, + value: weight(edge.source, edge.destination) + ) + } + } +} + +extension WeightedGraph { + /// Initializes a new `WeightedGraph` with a given graph and a weight function. + /// - Parameters: + /// - graph: The base graph. + /// - weight: A closure that calculates the weight of an edge given the source node, destination node, and the previous edge. + @inlinable public init( + graph: Graph, + weight: @escaping (Graph.Node, Graph.Node, PreviousEdge) -> Edge + ) where Graph == GridGraph { + self.graph = graph + self.weight = { source, destination in + weight(source, destination, graph.edge(source, destination)) + } + } +} + +extension GraphProtocol where Edge == Void { + /// Returns a `WeightedGraph` with the given weight function. + /// - Parameter weight: A closure that calculates the weight of an edge given the source and destination nodes. + /// - Returns: A `WeightedGraph` with the specified weight function. + @inlinable public func weighted(weight: @escaping (Node, Node) -> NewEdge) -> WeightedGraph { + WeightedGraph(graph: self, weight: weight) + } + + /// Returns a `WeightedGraph` with a constant weight for all edges. + /// - Parameter value: A closure that returns the constant weight for all edges. + /// - Returns: A `WeightedGraph` with the specified constant weight. + @inlinable public func weighted(constant value: @autoclosure @escaping () -> NewEdge) -> WeightedGraph { + WeightedGraph(graph: self) { _, _ in value() } + } +} + +extension GridGraph where Edge == Void { + /// Returns a `WeightedGraph` with weights calculated by the specified distance algorithm. + /// - Parameter distanceAlgorithm: The algorithm to use for calculating distances. Defaults to Euclidean distance. + /// - Returns: A `WeightedGraph` with weights calculated by the specified distance algorithm. + @inlinable public func weightedByDistance(_ distanceAlgorithm: DistanceAlgorithm = .euclideanDistance(of: \Self.Node.coordinates)) -> WeightedGraph { + WeightedGraph(graph: self, weight: distanceAlgorithm.distance) + } +} diff --git a/Sources/Graph/Distance.swift b/Sources/Graph/Distance.swift new file mode 100644 index 0000000..2617811 --- /dev/null +++ b/Sources/Graph/Distance.swift @@ -0,0 +1,48 @@ +/// A structure representing a distance algorithm between coordinates. +public struct DistanceAlgorithm where Distance: Numeric, Distance.Magnitude == Distance { + /// A closure that calculates the distance between two coordinates. + public let distance: (Coordinate, Coordinate) -> Distance + + /// Initializes a new distance algorithm with the given distance closure. + /// - Parameter distance: A closure that takes two coordinates and returns the distance between them. + @inlinable public init(distance: @escaping (Coordinate, Coordinate) -> Distance) { + self.distance = distance + } +} + +/// A type alias for a geometric distance algorithm where the distance is a scalar value of a SIMD type. +public typealias GeometricDistanceAlgorithm = DistanceAlgorithm where Value.Scalar: FloatingPoint + +extension DistanceAlgorithm { + /// Creates a geometric distance algorithm that calculates the Euclidean distance between coordinates. + /// - Parameter value: A closure that takes a coordinate and returns its SIMD value. + /// - Returns: A `GeometricDistanceAlgorithm` instance that calculates the Euclidean distance. + @inlinable public static func euclideanDistance(of value: @escaping (Coordinate) -> Value) -> Self where Self == GeometricDistanceAlgorithm { + .init { source, destination in + (value(source).squared() - value(destination).squared()).sum().squareRoot() + } + } + + /// Creates a geometric distance algorithm that calculates the Manhattan distance between coordinates. + /// - Parameter value: A closure that takes a coordinate and returns its SIMD value. + /// - Returns: A `GeometricDistanceAlgorithm` instance that calculates the Manhattan distance. + @inlinable public static func manhattanDistance(of value: @escaping (Coordinate) -> Value) -> Self where Self == GeometricDistanceAlgorithm { + .init { source, destination in + (value(source) - value(destination)).absoluteValue().sum() + } + } +} + +extension SIMD where Scalar: FloatingPoint { + /// Returns the element-wise absolute value of the SIMD vector. + /// - Returns: A SIMD vector where each element is the absolute value of the corresponding element in the original vector. + @usableFromInline func absoluteValue() -> Self { + pointwiseMax(.zero, self) + } + + /// Returns the element-wise square of the SIMD vector. + /// - Returns: A SIMD vector where each element is the square of the corresponding element in the original vector. + @usableFromInline func squared() -> Self { + self * self + } +} diff --git a/Sources/Graph/EulerianPath/Graph+EulerianPath+Backtracking.swift b/Sources/Graph/EulerianPath/Graph+EulerianPath+Backtracking.swift new file mode 100644 index 0000000..3bb2a8c --- /dev/null +++ b/Sources/Graph/EulerianPath/Graph+EulerianPath+Backtracking.swift @@ -0,0 +1,99 @@ +extension EulerianPathAlgorithm { + /// Creates a backtracking algorithm instance. + /// - Returns: An instance of `BacktrackingEulerianPathAlgorithm`. + @inlinable public static func backtracking() -> Self where Self == BacktrackingEulerianPathAlgorithm { + .init() + } +} + +/// An implementation of the backtracking algorithm for finding Eulerian paths and cycles in a graph. +public struct BacktrackingEulerianPathAlgorithm: EulerianPathAlgorithm { + /// Initializes a new `BacktrackingEulerianPathAlgorithm` instance. + @inlinable public init() {} + + /// Finds an Eulerian path from the source node to the destination node in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func findEulerianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + guard graph.hasEulerianPath() else { return nil } + return findEulerianSequence(from: source, to: destination, isCycle: false, in: graph) + } + + /// Finds an Eulerian cycle from the source node in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func findEulerianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + guard graph.hasEulerianCycle() else { return nil } + return findEulerianSequence(from: source, to: source, isCycle: true, in: graph) + } + + /// Finds an Eulerian sequence (path or cycle) in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - isCycle: A boolean indicating whether to find a cycle (true) or a path (false). + /// - graph: The graph in which to find the Eulerian sequence. + /// - Returns: A `Path` instance representing the Eulerian sequence, or `nil` if no sequence is found. + @usableFromInline func findEulerianSequence( + from source: Node, + to destination: Node, + isCycle: Bool, + in graph: some WholeGraphProtocol + ) -> Path? { + var adjacency: [Node: [GraphEdge]] = [:] + for edge in graph.allEdges { + adjacency[edge.source, default: []].append(edge) + } + + let totalEdges = graph.allEdges.count + var path: [GraphEdge] = [] + + /// A helper function to perform backtracking to find the Eulerian sequence. + /// - Parameter current: The current node being visited. + /// - Returns: A boolean indicating whether the Eulerian sequence has been found. + func backtrack(current: Node) -> Bool { + if path.count == totalEdges { + return current == destination + } + + guard let edges = adjacency[current], !edges.isEmpty else { + return false + } + + for (index, edge) in edges.enumerated() { + // Choose the edge + path.append(edge) + adjacency[current]?.remove(at: index) + + // Explore + if backtrack(current: edge.destination) { + return true + } + + // Un-choose (backtrack) + path.removeLast() + adjacency[current]?.insert(edge, at: index) + } + + return false + } + + if backtrack(current: source) { + return Path(source: source, destination: destination, edges: path) + } + + return nil + } +} diff --git a/Sources/Graph/EulerianPath/Graph+EulerianPath+Hierholzer.swift b/Sources/Graph/EulerianPath/Graph+EulerianPath+Hierholzer.swift new file mode 100644 index 0000000..492bd21 --- /dev/null +++ b/Sources/Graph/EulerianPath/Graph+EulerianPath+Hierholzer.swift @@ -0,0 +1,124 @@ +extension EulerianPathAlgorithm { + /// Creates a Hierholzer algorithm instance for graphs with numeric edge values. + /// - Returns: An instance of `HierholzerEulerianPathAlgorithm`. + @inlinable public static func hierholzer() -> Self where Self == HierholzerEulerianPathAlgorithm { + .init(defaultEdgeValue: .zero) + } + + /// Creates a Hierholzer algorithm instance for graphs with void edge values. + /// - Returns: An instance of `HierholzerEulerianPathAlgorithm`. + @inlinable public static func hierholzer() -> Self where Self == HierholzerEulerianPathAlgorithm { + .init(defaultEdgeValue: ()) + } +} + +/// An implementation of the Hierholzer algorithm for finding Eulerian paths and cycles in a graph. +public struct HierholzerEulerianPathAlgorithm: EulerianPathAlgorithm { + /// The default edge value used for temporary edges. + @usableFromInline let defaultEdgeValue: Edge + + /// Initializes a new `HierholzerEulerianPathAlgorithm` instance with the given default edge value. + /// - Parameter defaultEdgeValue: The default edge value used for temporary edges. + @inlinable public init(defaultEdgeValue: Edge) { + self.defaultEdgeValue = defaultEdgeValue + } + + /// Finds an Eulerian path from the source node to the destination node in the graph using the Hierholzer algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func findEulerianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + guard graph.hasEulerianPath() else { return nil } + + var adjacency = [Node: [GraphEdge]]() + for edge in graph.allEdges { + adjacency[edge.source, default: []].append(edge) + } + + let tempEdge = GraphEdge(source: destination, destination: source, value: defaultEdgeValue) + adjacency[destination, default: []].append(tempEdge) + + var stack = [source] + var circuit: [GraphEdge] = [] + + while !stack.isEmpty { + let current = stack.last! + + if var edges = adjacency[current], !edges.isEmpty { + let edge = edges.removeLast() + adjacency[current]! = edges + stack.append(edge.destination) + } else { + stack.removeLast() + if let last = stack.last, let edge = graph.allEdges.first(where: { $0.source == last && $0.destination == current }) { + circuit.append(edge) + } + } + } + + let orderedCircuit = Array(circuit.reversed()) + + guard let tempIndex = orderedCircuit.firstIndex(where: { $0.source == destination && $0.destination == source }) else { + return nil + } + + let pathEdges = orderedCircuit[tempIndex + 1 ..< orderedCircuit.endIndex] + orderedCircuit[0 ..< tempIndex] + + let finalPathEdges = pathEdges.filter { !($0.source == destination && $0.destination == source) } + + if finalPathEdges.count == graph.allEdges.count { + return Path(source: source, destination: destination, edges: Array(finalPathEdges)) + } else { + return nil + } + } + + /// Finds an Eulerian cycle from the source node in the graph using the Hierholzer algorithm. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func findEulerianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + guard graph.hasEulerianCycle() else { return nil } + + var adjacency = [Node: [GraphEdge]]() + for edge in graph.allEdges { + adjacency[edge.source, default: []].append(edge) + } + + var stack = [source] + var circuit: [GraphEdge] = [] + + while !stack.isEmpty { + let current = stack.last! + + if var edges = adjacency[current], !edges.isEmpty { + let edge = edges.removeLast() + adjacency[current]! = edges + stack.append(edge.destination) + } else { + stack.removeLast() + if let last = stack.last, let edge = graph.allEdges.first(where: { $0.source == last && $0.destination == current }) { + circuit.append(edge) + } + } + } + + let orderedCircuit = circuit.reversed() + + if orderedCircuit.count == graph.allEdges.count { + return Path(source: source, destination: source, edges: Array(orderedCircuit)) + } else { + return nil + } + } +} diff --git a/Sources/Graph/EulerianPath/Graph+EulerianPath.swift b/Sources/Graph/EulerianPath/Graph+EulerianPath.swift new file mode 100644 index 0000000..7bd2d0f --- /dev/null +++ b/Sources/Graph/EulerianPath/Graph+EulerianPath.swift @@ -0,0 +1,200 @@ +extension WholeGraphProtocol { + /// Finds an Eulerian path from the source to the destination using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The ending node. + /// - algorithm: The algorithm to use for finding the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath>( + from source: Node, + to destination: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findEulerianPath(from: source, to: destination, in: self) + } + + /// Finds an Eulerian path from the source to the destination using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The ending node. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath( + from source: Node, + to destination: Node + ) -> Path? where Node: Hashable, Edge: Hashable { + eulerianPath(from: source, to: destination, using: .backtracking()) + } + + /// Finds an Eulerian path from the source node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath>( + from source: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findEulerianPath(from: source, in: self) + } + + /// Finds an Eulerian path from the source node using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath( + from source: Node + ) -> Path? where Node: Hashable, Edge: Hashable { + eulerianPath(from: source, using: .backtracking()) + } + + /// Finds an Eulerian path using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath>( + using algorithm: Algorithm + ) -> Path? { + algorithm.findEulerianPath(in: self) + } + + /// Finds an Eulerian path using the default backtracking algorithm. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func eulerianPath() -> Path? where Node: Hashable, Edge: Hashable { + eulerianPath(using: .backtracking()) + } + + /// Finds an Eulerian cycle from the source node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func eulerianCycle>( + from source: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findEulerianCycle(from: source, in: self) + } + + /// Finds an Eulerian cycle from the source node using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func eulerianCycle( + from source: Node + ) -> Path? where Node: Hashable, Edge: Hashable { + eulerianCycle(from: source, using: .backtracking()) + } + + /// Finds an Eulerian cycle using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func eulerianCycle>( + using algorithm: Algorithm + ) -> Path? { + algorithm.findEulerianCycle(in: self) + } + + + /// Finds an Eulerian cycle using the default backtracking algorithm. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func eulerianCycle() -> Path? where Node: Hashable, Edge: Hashable { + eulerianCycle(using: .backtracking()) + } +} + +/// A protocol defining the requirements for an Eulerian path algorithm. +public protocol EulerianPathAlgorithm { + associatedtype Node: Hashable + associatedtype Edge + + /// Finds an Eulerian path from the source to the destination in the given graph. + /// - Parameters: + /// - source: The starting node. + /// - destination: The ending node. + /// - graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable func findEulerianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds an Eulerian path from the source in the given graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable func findEulerianPath( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds an Eulerian path in the given graph. + /// - Parameter graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable func findEulerianPath( + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds an Eulerian cycle from the source in the given graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable func findEulerianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds an Eulerian cycle in the given graph. + /// - Parameter graph: The graph in which to find the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable func findEulerianCycle( + in graph: some WholeGraphProtocol + ) -> Path? +} + +extension EulerianPathAlgorithm { + /// Finds an Eulerian path in the graph. + /// - Parameter graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func findEulerianPath(in graph: some WholeGraphProtocol) -> Path? { + guard graph.hasEulerianPath() else { return nil } + + for source in graph.allNodes { + if let path = findEulerianPath(from: source, in: graph) { + return path + } + } + return nil + } + + /// Finds an Eulerian cycle in the graph. + /// - Parameter graph: The graph in which to find the Eulerian cycle. + /// - Returns: A `Path` instance representing the Eulerian cycle, or `nil` if no cycle is found. + @inlinable public func findEulerianCycle(in graph: some WholeGraphProtocol) -> Path? { + guard graph.hasEulerianCycle() else { return nil } + + for startNode in graph.allNodes { + if let cycle = findEulerianCycle(from: startNode, in: graph) { + return cycle + } + } + return nil + } + + /// Finds an Eulerian path from the source node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Eulerian path. + /// - Returns: A `Path` instance representing the Eulerian path, or `nil` if no path is found. + @inlinable public func findEulerianPath(from source: Node, in graph: some WholeGraphProtocol) -> Path? { + guard graph.hasEulerianPath() else { return nil } + + for destination in graph.allNodes where destination != source { + if let path = findEulerianPath(from: source, to: destination, in: graph) { + return path + } + } + return nil + } +} diff --git a/Sources/Graph/GraphProtocol.swift b/Sources/Graph/GraphProtocol.swift new file mode 100644 index 0000000..9c10124 --- /dev/null +++ b/Sources/Graph/GraphProtocol.swift @@ -0,0 +1,68 @@ +/// A protocol that defines the basic requirements for a graph structure. +public protocol GraphProtocol { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. Defaults to `Void`. + associatedtype Edge = Void + + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable func edges(from node: Node) -> [GraphEdge] +} + +/// A structure representing an edge in a graph. +public struct GraphEdge { + /// The source node of the edge. + public let source: Node + /// The destination node of the edge. + public let destination: Node + /// The value associated with the edge. + public let value: Value + + /// Initializes a new graph edge with the given source, destination, and value. + /// - Parameters: + /// - source: The source node of the edge. + /// - destination: The destination node of the edge. + /// - value: The value associated with the edge. Defaults to `()`. + @inlinable public init(source: Node, destination: Node, value: Value = ()) { + self.source = source + self.destination = destination + self.value = value + } + + /// Returns a new edge with the source and destination nodes reversed. + @inlinable public var reversed: Self { + GraphEdge(source: destination, destination: source, value: value) + } + + /// Transforms the source and destination nodes of the edge using the provided closure. + @inlinable public func mapNode(_ transform: (Node) -> NewNode) -> GraphEdge { + .init(source: transform(source), destination: transform(destination), value: value) + } + + /// Transforms the value of the edge using the provided closure. + @inlinable public func mapEdge(_ transform: (Value) -> NewValue) -> GraphEdge { + .init(source: source, destination: destination, value: transform(value)) + } +} + +extension GraphEdge: Equatable where Node: Equatable, Value: Equatable {} +extension GraphEdge: Hashable where Node: Hashable, Value: Hashable {} +extension GraphEdge: Comparable where Node: Equatable, Value: Equatable, Value: Weighted { + /// Compares two graph edges based on their weights. + /// - Parameters: + /// - lhs: The left-hand side edge. + /// - rhs: The right-hand side edge. + /// - Returns: `true` if the weight of the left-hand side edge is less than the weight of the right-hand side edge, `false` otherwise. + @inlinable public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.value.weight < rhs.value.weight + } +} + +extension GraphEdge: Weighted where Value: Weighted { + /// The weight of the edge. + @inlinable public var weight: Value.Weight { + value.weight + } +} diff --git a/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath+Backtracking.swift b/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath+Backtracking.swift new file mode 100644 index 0000000..afd4b60 --- /dev/null +++ b/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath+Backtracking.swift @@ -0,0 +1,117 @@ +extension HamiltonianPathAlgorithm { + /// Creates a backtracking algorithm instance. + /// - Returns: An instance of `BacktrackingHamiltonianPathAlgorithm`. + @inlinable public static func backtracking() -> Self where Self == BacktrackingHamiltonianPathAlgorithm { + .init() + } +} + +/// An implementation of the backtracking algorithm for finding Hamiltonian paths and cycles in a graph. +public struct BacktrackingHamiltonianPathAlgorithm: HamiltonianPathAlgorithm { + /// Initializes a new `BacktrackingHamiltonianPathAlgorithm` instance. + @inlinable public init() {} + + /// Finds a Hamiltonian path from the source node to the destination node in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func findHamiltonianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + findHamiltonianSequence(from: source, to: destination, isCycle: false, in: graph) + } + + /// Finds a Hamiltonian cycle from the source node in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func findHamiltonianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + findHamiltonianSequence(from: source, to: source, isCycle: true, in: graph) + } + + /// Finds a Hamiltonian sequence (path or cycle) in the graph using the backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - isCycle: A boolean indicating whether to find a cycle (true) or a path (false). + /// - graph: The graph in which to find the Hamiltonian sequence. + /// - Returns: A `Path` instance representing the Hamiltonian sequence, or `nil` if no sequence is found. + @usableFromInline func findHamiltonianSequence( + from source: Node, + to destination: Node, + isCycle: Bool, + in graph: some WholeGraphProtocol + ) -> Path? { + let allNodes = Set(graph.allNodes) + var path: [Node] = [source] + var visited = Set([source]) + var resultPath: [GraphEdge]? + + /// A helper function to perform backtracking to find the Hamiltonian sequence. + /// - Parameter current: The current node being visited. + func backtrack(current: Node) { + if resultPath != nil { + return + } + + if visited.count == allNodes.count { + if isCycle { + if let closingEdge = graph.edges(from: current).first(where: { $0.destination == source }) { + let edges = constructEdges(from: path, withCycle: true, closingEdge: closingEdge) + resultPath = edges + } + } else { + let edges = constructEdges(from: path, withCycle: false, closingEdge: nil) + resultPath = edges + } + return + } + + for edge in graph.edges(from: current) { + let neighbor = edge.destination + if !visited.contains(neighbor) { + // Choose + visited.insert(neighbor) + path.append(neighbor) + + // Explore + backtrack(current: neighbor) + + // Un-choose (backtrack) + visited.remove(neighbor) + path.removeLast() + } + } + } + + func constructEdges(from nodes: [Node], withCycle: Bool, closingEdge: GraphEdge?) -> [GraphEdge] { + var edges: [GraphEdge] = [] + for i in 0..(_ heuristic: Self.Heuristic) -> Self where Self == HeuristicHamiltonianPathAlgorithm { + .init(heuristic: heuristic) + } +} + +/// An implementation of a heuristic algorithm for finding Hamiltonian paths and cycles in a graph. +public struct HeuristicHamiltonianPathAlgorithm: HamiltonianPathAlgorithm { + /// A structure representing the heuristic function used by the algorithm. + public struct Heuristic { + /// A closure that evaluates a node based on the current path and the graph. + public let evaluate: (_ node: Node, _ path: [Node], _ graph: any WholeGraphProtocol) -> Double + + /// Initializes a new `Heuristic` instance with the given evaluation closure. + /// - Parameter evaluate: A closure that evaluates a node based on the current path and the graph. + @inlinable public init(evaluate: @escaping (Node, [Node], any WholeGraphProtocol) -> Double) { + self.evaluate = evaluate + } + } + + /// The heuristic function used by the algorithm. + public let heuristic: Heuristic + + /// Initializes a new `HeuristicHamiltonianPathAlgorithm` instance with the given heuristic. + /// - Parameter heuristic: The heuristic function to use for evaluating nodes. + @inlinable public init(heuristic: Heuristic) { + self.heuristic = heuristic + } + + /// Finds a Hamiltonian path from the source node to the destination node in the graph using the heuristic algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func findHamiltonianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + findHamiltonianSequence(from: source, to: destination, isCycle: false, in: graph) + } + + /// Finds a Hamiltonian cycle from the source node in the graph using the heuristic algorithm. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func findHamiltonianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + findHamiltonianSequence(from: source, to: source, isCycle: true, in: graph) + } + + /// Finds a Hamiltonian sequence (path or cycle) in the graph using the heuristic algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - isCycle: A boolean indicating whether to find a cycle (true) or a path (false). + /// - graph: The graph in which to find the Hamiltonian sequence. + /// - Returns: A `Path` instance representing the Hamiltonian sequence, or `nil` if no sequence is found. + @usableFromInline func findHamiltonianSequence( + from source: Node, + to destination: Node, + isCycle: Bool, + in graph: some WholeGraphProtocol + ) -> Path? { + var openSet = Heap() + let initialHeuristic = heuristic.evaluate(source, [source], graph) + openSet.insert(State(node: source, path: [source], visited: [source], estimatedTotalCost: initialHeuristic)) + + while let currentState = openSet.popMin() { + let currentNode = currentState.node + + if currentState.visited.count == graph.allNodes.count { + if isCycle { + if graph.edges(from: currentNode).contains(where: { $0.destination == source }) { + // Reconstruct the path and append the closing edge + let edges = constructEdges(from: currentState.path, withCycle: true, graph: graph) + return Path(source: source, destination: destination, edges: edges) + } + } else { + let edges = constructEdges(from: currentState.path, withCycle: false, graph: graph) + return Path(source: source, destination: destination, edges: edges) + } + continue + } + + // Explore all unvisited neighbors + for edge in graph.edges(from: currentNode) { + let neighbor = edge.destination + if currentState.visited.contains(neighbor) { + continue + } + + // Create a new path by appending the neighbor + var newPath = currentState.path + newPath.append(neighbor) + + // Update the visited set + var newVisited = currentState.visited + newVisited.insert(neighbor) + + // Calculate the heuristic for the neighbor + let heuristicValue = heuristic.evaluate(neighbor, newPath, graph) + + // Create a new state and add it to the open set + let newState = State( + node: neighbor, + path: newPath, + visited: newVisited, + estimatedTotalCost: Double(newPath.count) + heuristicValue + ) + openSet.insert(newState) + } + } + + return nil + } + + @usableFromInline func constructEdges( + from nodes: [Node], + withCycle: Bool, + graph: any WholeGraphProtocol + ) -> [GraphEdge] { + var edges: [GraphEdge] = [] + for i in 0.. + @usableFromInline let estimatedTotalCost: Double + + @inlinable init(node: Node, path: [Node], visited: Set, estimatedTotalCost: Double) { + self.node = node + self.path = path + self.visited = visited + self.estimatedTotalCost = estimatedTotalCost + } + + @inlinable static func < (lhs: State, rhs: State) -> Bool { + lhs.estimatedTotalCost < rhs.estimatedTotalCost + } + } +} + +extension HeuristicHamiltonianPathAlgorithm.Heuristic { + @inlinable public static func degree() -> Self { + .init { node, _, graph in + Double(graph.edges(from: node).count) + } + } + + @inlinable public static func distance(distanceAlgorithm: DistanceAlgorithm) -> Self { + .init { node, path, graph in + let visited = Set(path) + let unvisited = graph.allNodes.filter { !visited.contains($0) } + guard !unvisited.isEmpty else { return .zero } + + let totalDistance = unvisited.reduce(.zero) { sum, unvisitedNode in + return sum + distanceAlgorithm.distance(node, unvisitedNode) + } + return -totalDistance / Double(unvisited.count) + } + } +} diff --git a/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath.swift b/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath.swift new file mode 100644 index 0000000..f4d5f17 --- /dev/null +++ b/Sources/Graph/HamiltonianPath/Graph+HamiltonianPath.swift @@ -0,0 +1,195 @@ +extension WholeGraphProtocol { + /// Finds a Hamiltonian path from the source node to the destination node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - algorithm: The algorithm to use for finding the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath>( + from source: Node, + to destination: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findHamiltonianPath(from: source, to: destination, in: self) + } + + /// Finds a Hamiltonian path from the source node to the destination node using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath( + from source: Node, + to destination: Node + ) -> Path? where Node: Hashable { + hamiltonianPath(from: source, to: destination, using: .backtracking()) + } + + /// Finds a Hamiltonian path from the source node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath>( + from source: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findHamiltonianPath(from: source, in: self) + } + + /// Finds a Hamiltonian path from the source node using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath( + from source: Node + ) -> Path? where Node: Hashable { + hamiltonianPath(from: source, using: .backtracking()) + } + + /// Finds a Hamiltonian cycle from the source node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func hamiltonianCycle>( + from source: Node, + using algorithm: Algorithm + ) -> Path? { + algorithm.findHamiltonianCycle(from: source, in: self) + } + + /// Finds a Hamiltonian cycle from the source node using the default backtracking algorithm. + /// - Parameters: + /// - source: The starting node. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func hamiltonianCycle( + from source: Node + ) -> Path? where Node: Hashable { + hamiltonianCycle(from: source, using: .backtracking()) + } + + /// Finds a Hamiltonian path using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath>( + using algorithm: Algorithm + ) -> Path? { + algorithm.findHamiltonianPath(in: self) + } + + /// Finds a Hamiltonian path using the default backtracking algorithm. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func hamiltonianPath() -> Path? where Node: Hashable { + hamiltonianPath(using: .backtracking()) + } + + /// Finds a Hamiltonian cycle using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func hamiltonianCycle>( + using algorithm: Algorithm + ) -> Path? { + algorithm.findHamiltonianCycle(in: self) + } + + /// Finds a Hamiltonian cycle using the default backtracking algorithm. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func hamiltonianCycle() -> Path? where Node: Hashable { + hamiltonianCycle(using: .backtracking()) + } +} + +/// A protocol that defines the requirements for a Hamiltonian path algorithm. +public protocol HamiltonianPathAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Finds a Hamiltonian path from the source node to the destination node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable func findHamiltonianPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds a Hamiltonian path from the source node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable func findHamiltonianPath( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds a Hamiltonian path in the graph. + /// - Parameter graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable func findHamiltonianPath( + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds a Hamiltonian cycle from the source node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable func findHamiltonianCycle( + from source: Node, + in graph: some WholeGraphProtocol + ) -> Path? + + /// Finds a Hamiltonian cycle in the graph. + /// - Parameter graph: The graph in which to find the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable func findHamiltonianCycle( + in graph: some WholeGraphProtocol + ) -> Path? +} + +extension HamiltonianPathAlgorithm { + /// Finds a Hamiltonian path in the graph. + /// - Parameter graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func findHamiltonianPath(in graph: some WholeGraphProtocol) -> Path? { + for source in graph.allNodes { + if let path = findHamiltonianPath(from: source, in: graph) { + return path + } + } + return nil + } + + /// Finds a Hamiltonian cycle in the graph. + /// - Parameter graph: The graph in which to find the Hamiltonian cycle. + /// - Returns: A `Path` instance representing the Hamiltonian cycle, or `nil` if no cycle is found. + @inlinable public func findHamiltonianCycle(in graph: some WholeGraphProtocol) -> Path? { + for startNode in graph.allNodes { + if let cycle = findHamiltonianCycle(from: startNode, in: graph) { + return cycle + } + } + return nil + } + + /// Finds a Hamiltonian path from the source node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - graph: The graph in which to find the Hamiltonian path. + /// - Returns: A `Path` instance representing the Hamiltonian path, or `nil` if no path is found. + @inlinable public func findHamiltonianPath(from source: Node, in graph: some WholeGraphProtocol) -> Path? { + for destination in graph.allNodes where destination != source { + if let path = findHamiltonianPath(from: source, to: destination, in: graph) { + return path + } + } + return nil + } +} diff --git a/Sources/Graph/Matching/MaximumMatchingAlgorithm+HopcroftKarp.swift b/Sources/Graph/Matching/MaximumMatchingAlgorithm+HopcroftKarp.swift new file mode 100644 index 0000000..7ef08a1 --- /dev/null +++ b/Sources/Graph/Matching/MaximumMatchingAlgorithm+HopcroftKarp.swift @@ -0,0 +1,121 @@ +extension MaximumMatchingAlgorithm { + /// Creates a new instance of the Hopcroft-Karp algorithm. + @inlinable public static func hopcroftKarp() -> Self where Self == HopcroftKarpAlgorithm { + HopcroftKarpAlgorithm() + } +} + +/// An implementation of the Hopcroft-Karp algorithm for finding the maximum matching in a bipartite graph. +public struct HopcroftKarpAlgorithm: MaximumMatchingAlgorithm { + /// Initializes a new `HopcroftKarpAlgorithm` instance. + @inlinable public init() {} + + /// Finds the maximum matching in the graph using the Hopcroft-Karp algorithm. + @inlinable public func maximumMatching( + in graph: some BipartiteGraphProtocol + ) -> [Node: Node] { + let leftPartition = graph.leftPartition + let rightPartition = graph.rightPartition + + var pairU: [Node: Node?] = [:] // Matching for left nodes (U) + var pairV: [Node: Node?] = [:] // Matching for right nodes (V) + var dist: [Node: Int] = [:] + + // Initialize pairings to nil + for u in leftPartition { + pairU[u] = nil + } + for v in rightPartition { + pairV[v] = nil + } + + while bfs(graph: graph, pairU: &pairU, pairV: &pairV, dist: &dist) { + for u in leftPartition { + if pairU[u] == nil { + _ = dfs(u: u, graph: graph, pairU: &pairU, pairV: &pairV, dist: &dist) + } + } + } + + // Build the matching from pairU + var matching: [Node: Node] = [:] + for (u, vOpt) in pairU { + if let v = vOpt { + matching[u] = v + } + } + + return matching + } + + /// Breadth-first search for finding augmenting paths. + @usableFromInline func bfs( + graph: some BipartiteGraphProtocol, + pairU: inout [Node: Node?], + pairV: inout [Node: Node?], + dist: inout [Node: Int] + ) -> Bool { + var queue: [Node] = [] + var distNil = Int.max + + for u in graph.leftPartition { + if pairU[u] == nil { + dist[u] = 0 + queue.append(u) + } else { + dist[u] = Int.max + } + } + + while !queue.isEmpty { + let u = queue.removeFirst() + if dist[u]! < distNil { + for edge in graph.edges(from: u) { + let v = edge.destination + if let pairVv = pairV[v] ?? nil { + // v is matched + if dist[pairVv] == nil || dist[pairVv]! == Int.max { + dist[pairVv] = dist[u]! + 1 + queue.append(pairVv) + } + } else { + // v is unmatched + distNil = dist[u]! + 1 + } + } + } + } + + return distNil != Int.max + } + + /// Depth-first search for finding augmenting paths. + @usableFromInline func dfs( + u: Node, + graph: some BipartiteGraphProtocol, + pairU: inout [Node: Node?], + pairV: inout [Node: Node?], + dist: inout [Node: Int] + ) -> Bool { + for edge in graph.edges(from: u) { + let v = edge.destination + if let pairVv = pairV[v] ?? nil { + // v is matched + if dist[pairVv] == dist[u]! + 1 { + if dfs(u: pairVv, graph: graph, pairU: &pairU, pairV: &pairV, dist: &dist) { + pairU[u] = v + pairV[v] = u + return true + } + } + } else { + // v is unmatched + pairU[u] = v + pairV[v] = u + return true + } + } + dist[u] = Int.max + return false + } +} diff --git a/Sources/Graph/Matching/MaximumMatchingAlgorithm.swift b/Sources/Graph/Matching/MaximumMatchingAlgorithm.swift new file mode 100644 index 0000000..531a02a --- /dev/null +++ b/Sources/Graph/Matching/MaximumMatchingAlgorithm.swift @@ -0,0 +1,25 @@ +extension BipartiteGraphProtocol where Node: Hashable { + /// Finds the maximum matching in the graph using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the maximum matching. + /// - Returns: A dictionary where the keys are the nodes in the first partition and the values are the nodes in the second partition. + @inlinable public func maximumMatching( + using algorithm: Algorithm + ) -> [Node: Node] where Algorithm.Node == Node, Algorithm.Edge == Edge { + algorithm.maximumMatching(in: self) + } +} + +/// A protocol that defines the requirements for a maximum matching algorithm. +public protocol MaximumMatchingAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the maximum matching in the graph. + /// - Parameter graph: The graph in which to find the maximum matching. + /// - Returns: A dictionary where the keys are the nodes in the first partition and the values are the nodes in the second partition. + @inlinable func maximumMatching( + in graph: some BipartiteGraphProtocol + ) -> [Node: Node] +} diff --git a/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+Dinic.swift b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+Dinic.swift new file mode 100644 index 0000000..c26bdd1 --- /dev/null +++ b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+Dinic.swift @@ -0,0 +1,142 @@ +extension MaxFlowAlgorithm { + /// Creates a new instance of the Dinic's algorithm. + @inlinable public static func dinic() -> Self where Self == DinicAlgorithm { + DinicAlgorithm() + } +} + +/// An implementation of Dinic's algorithm for computing the maximum flow in a graph. +public struct DinicAlgorithm: MaxFlowAlgorithm where Edge.Weight: Numeric & Comparable & FixedWidthInteger { + @inlinable public init() {} + + /// Computes the maximum flow in the graph from the source node to the sink node using Dinic's algorithm. + @inlinable public func maximumFlow( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> Edge.Weight { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while true { + guard let level = buildLevelGraph(residualGraph: &residual, source: source, sink: sink) else { + break + } + var nextEdgeIndex: [Node: Int] = [:] + var flow: Edge.Weight + repeat { + flow = sendFlowDFS( + residualGraph: &residual, + current: source, + sink: sink, + flow: Edge.Weight.max, + level: level, + nextEdgeIndex: &nextEdgeIndex + ) + maxFlow += flow + } while flow > .zero + } + return maxFlow + } + + /// Computes the minimum cut in the graph from the source node to the sink node using Dinic's algorithm. + @inlinable public func minimumCut( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> (cutValue: Edge.Weight, cutEdges: Set>) { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while true { + guard let level = buildLevelGraph(residualGraph: &residual, source: source, sink: sink) else { + break + } + var nextEdgeIndex: [Node: Int] = [:] + var flow: Edge.Weight + repeat { + flow = sendFlowDFS( + residualGraph: &residual, + current: source, + sink: sink, + flow: Edge.Weight.max, + level: level, + nextEdgeIndex: &nextEdgeIndex + ) + maxFlow += flow + } while flow > .zero + } + + let reachable = residual.reachableNodes(from: source) + let cutEdges = graph.allEdges.filter { edge in + reachable.contains(edge.source) && !reachable.contains(edge.destination) + } + return (cutValue: maxFlow, cutEdges: Set(cutEdges)) + } + + /// Builds a level graph for the given residual graph. + @usableFromInline + func buildLevelGraph( + residualGraph: inout ResidualGraph>, + source: Node, + sink: Node + ) -> [Node: Int]? { + var level: [Node: Int] = [:] + level[source] = 0 + var queue: [Node] = [source] + + while !queue.isEmpty { + let current = queue.removeFirst() + let currentLevel = level[current]! + for edge in residualGraph.edges(from: current) { + if edge.value > .zero && level[edge.destination] == nil { + level[edge.destination] = currentLevel + 1 + queue.append(edge.destination) + } + } + } + return level[sink] != nil ? level : nil + } + + /// Sends flow through the graph using a depth-first search. + @usableFromInline + func sendFlowDFS( + residualGraph: inout ResidualGraph>, + current: Node, + sink: Node, + flow currentFlow: Edge.Weight, + level: [Node: Int], + nextEdgeIndex: inout [Node: Int] + ) -> Edge.Weight { + if current == sink { + return currentFlow + } + + let edges = residualGraph.edges(from: current) + var edgeIndex = nextEdgeIndex[current] ?? 0 + + while edgeIndex < edges.count { + let edge = edges[edgeIndex] + if let levelU = level[current], let levelV = level[edge.destination], levelV == levelU + 1 { + if edge.value > .zero { + let minFlow = min(currentFlow, edge.value) + let flow = sendFlowDFS( + residualGraph: &residualGraph, + current: edge.destination, + sink: sink, + flow: minFlow, + level: level, + nextEdgeIndex: &nextEdgeIndex + ) + if flow > .zero { + residualGraph.addFlow(from: current, to: edge.destination, flow: flow) + return flow + } + } + } + edgeIndex += 1 + nextEdgeIndex[current] = edgeIndex + } + return .zero + } +} diff --git a/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+EdmondsKarp.swift b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+EdmondsKarp.swift new file mode 100644 index 0000000..5918c1c --- /dev/null +++ b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+EdmondsKarp.swift @@ -0,0 +1,65 @@ +extension MaxFlowAlgorithm { + /// Creates an Edmonds-Karp algorithm instance. + /// - Returns: An instance of `EdmondsKarpAlgorithm`. + @inlinable public static func edmondsKarp() -> Self where Self == EdmondsKarpAlgorithm { + EdmondsKarpAlgorithm() + } +} + +/// An implementation of the Edmonds-Karp algorithm for finding the maximum flow in a graph. +public struct EdmondsKarpAlgorithm: MaxFlowAlgorithm where Edge.Weight: Numeric { + /// Initializes a new `EdmondsKarpAlgorithm` instance. + @inlinable public init() {} + + /// Computes the maximum flow in the graph from the source node to the sink node using the Edmonds-Karp algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the maximum flow. + /// - Returns: The maximum flow value from the source node to the sink node. + @inlinable public func maximumFlow( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> Edge.Weight { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while let path = residual.searchFirst(from: source, strategy: .bfs(.trackPath()), goal: { $0.node == sink }) { + let flow = path.edges.map { residual.residualCapacity(from: $0.source, to: $0.destination) }.min() ?? .zero + maxFlow += flow + residual.addFlow(path: path.path, flow: flow) + } + + return maxFlow + } + + /// Computes the minimum cut in the graph from the source node to the sink node using the Edmonds-Karp algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the minimum cut. + /// - Returns: A tuple containing the cut value and the set of edges in the minimum cut. + @inlinable public func minimumCut( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> (cutValue: Edge.Weight, cutEdges: Set>) { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while let path = residual.searchFirst(from: source, strategy: .bfs(.trackPath()), goal: { $0.node == sink }) { + let flow = path.edges.map { residual.residualCapacity(from: $0.source, to: $0.destination) }.min() ?? .zero + maxFlow += flow + residual.addFlow(path: path.path, flow: flow) + } + + let reachable = residual.reachableNodes(from: source) + + let cutEdges = graph.allEdges.filter { edge in + reachable.contains(edge.source) && !reachable.contains(edge.destination) + } + + return (cutValue: maxFlow, cutEdges: Set(cutEdges)) + } +} diff --git a/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+FordFulkerson.swift b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+FordFulkerson.swift new file mode 100644 index 0000000..ad45dd5 --- /dev/null +++ b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow+FordFulkerson.swift @@ -0,0 +1,66 @@ +import Collections + +extension MaxFlowAlgorithm { + /// Creates a Ford-Fulkerson algorithm instance. + /// - Returns: An instance of `FordFulkersonAlgorithm`. + @inlinable public static func fordFulkerson() -> Self where Self == FordFulkersonAlgorithm { + FordFulkersonAlgorithm() + } +} + +/// An implementation of the Ford-Fulkerson algorithm for finding the maximum flow in a graph. +public struct FordFulkersonAlgorithm: MaxFlowAlgorithm where Edge.Weight: Numeric { + /// Initializes a new `FordFulkersonAlgorithm` instance. + @inlinable public init() {} + + /// Computes the maximum flow in the graph from the source node to the sink node using the Ford-Fulkerson algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the maximum flow. + /// - Returns: The maximum flow value from the source node to the sink node. + @inlinable public func maximumFlow( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> Edge.Weight { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while let path = residual.searchFirst(from: source, strategy: .dfs(.trackPath()), goal: { $0.node == sink }) { + let flow = path.edges.map { residual.residualCapacity(from: $0.source, to: $0.destination) }.min() ?? .zero + maxFlow += flow + residual.addFlow(path: path.path, flow: flow) + } + + return maxFlow + } + + /// Computes the minimum cut in the graph from the source node to the sink node using the Ford-Fulkerson algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the minimum cut. + /// - Returns: A tuple containing the cut value and the set of edges in the minimum cut. + @inlinable public func minimumCut( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> (cutValue: Edge.Weight, cutEdges: Set>) { + var residual = ResidualGraph(base: graph) + var maxFlow: Edge.Weight = .zero + + while let path = residual.searchFirst(from: source, strategy: .dfs(.trackPath()), goal: { $0.node == sink }) { + let flow = path.edges.map { residual.residualCapacity(from: $0.source, to: $0.destination) }.min() ?? .zero + maxFlow += flow + residual.addFlow(path: path.path, flow: flow) + } + + let reachable = residual.reachableNodes(from: source) + let cutEdges = graph.allEdges.filter { edge in + reachable.contains(edge.source) && !reachable.contains(edge.destination) + } + + return (cutValue: maxFlow, cutEdges: Set(cutEdges)) + } +} diff --git a/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow.swift b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow.swift new file mode 100644 index 0000000..549422d --- /dev/null +++ b/Sources/Graph/MaxFlow-MinCut/Graph+MaxFlow.swift @@ -0,0 +1,77 @@ +extension WholeGraphProtocol where Edge: Weighted & Comparable, Node: Hashable { + /// Computes the maximum flow from the source node to the sink node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - algorithm: The algorithm to use for computing the maximum flow. + /// - Returns: The maximum flow value from the source node to the sink node. + @inlinable public func maximumFlow( + from source: Node, + to sink: Node, + using algorithm: Algorithm + ) -> Edge.Weight where Algorithm.Node == Node, Algorithm.Edge == Edge { + algorithm.maximumFlow(from: source, to: sink, in: self) + } + + /// Computes the minimum cut from the source node to the sink node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - algorithm: The algorithm to use for computing the minimum cut. + /// - Returns: A tuple containing the cut value and the set of edges in the minimum cut. + @inlinable public func minimumCut( + from source: Node, + to sink: Node, + using algorithm: Algorithm + ) -> (cutValue: Edge.Weight, cutEdges: Set>) where Algorithm.Node == Node, Algorithm.Edge == Edge { + algorithm.minimumCut(from: source, to: sink, in: self) + } +} + +/// A protocol that defines the requirements for a maximum flow algorithm. +public protocol MaxFlowAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph, which must conform to the `Weighted` protocol. + associatedtype Edge: Hashable & Weighted + + /// Computes the maximum flow in the graph from the source node to the sink node. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the maximum flow. + /// - Returns: The maximum flow value from the source node to the sink node. + func maximumFlow( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> Edge.Weight + + /// Computes the minimum cut in the graph from the source node to the sink node. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the minimum cut. + /// - Returns: A tuple containing the cut value and the set of edges in the minimum cut. + func minimumCut( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> (cutValue: Edge.Weight, cutEdges: Set>) +} + +extension MaxFlowAlgorithm { + /// Computes the maximum flow in the graph from the source node to the sink node. + /// - Parameters: + /// - source: The starting node. + /// - sink: The target node. + /// - graph: The graph in which to compute the maximum flow. + /// - Returns: The maximum flow value from the source node to the sink node. + @inlinable public func maximumFlow( + from source: Node, + to sink: Node, + in graph: some WholeGraphProtocol + ) -> Edge.Weight { + minimumCut(from: source, to: sink, in: graph).cutValue // ha! + } +} diff --git a/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Boruvka.swift b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Boruvka.swift new file mode 100644 index 0000000..407e809 --- /dev/null +++ b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Boruvka.swift @@ -0,0 +1,71 @@ +extension MinimumSpanningTreeAlgorithm { + /// Creates a Boruvka algorithm instance. + /// - Returns: An instance of `BoruvkaAlgorithm`. + @inlinable public static func boruvka() -> Self where Self == BoruvkaAlgorithm { + BoruvkaAlgorithm() + } +} + +/// An implementation of the Boruvka algorithm for finding the minimum spanning tree in a graph. +public struct BoruvkaAlgorithm: MinimumSpanningTreeAlgorithm where Edge.Weight: Comparable { + /// Initializes a new `BoruvkaAlgorithm` instance. + @inlinable public init() {} + + /// Finds the minimum spanning tree in the graph using the Boruvka algorithm. + /// - Parameter graph: The graph in which to find the minimum spanning tree. + /// - Returns: An array of `GraphEdge` instances representing the edges in the minimum spanning tree. + @inlinable public func minimumSpanningTree(in graph: some WholeGraphProtocol) -> [GraphEdge] { + var uf = UnionFind() + var mst: [GraphEdge] = [] + + for node in graph.allNodes { + uf.add(node) + } + + var numComponents = graph.allNodes.count + + while numComponents > 1 { + var cheapest: [Node: GraphEdge] = [:] + + // Find cheapest edge for each component + for edge in graph.allEdges { + let u = edge.source + let v = edge.destination + let setU = uf.find(u) + let setV = uf.find(v) + + if setU != setV { + if let currentEdge = cheapest[setU] { + if edge.value.weight < currentEdge.value.weight { + cheapest[setU] = edge + } + } else { + cheapest[setU] = edge + } + if let currentEdge = cheapest[setV] { + if edge.value.weight < currentEdge.value.weight { + cheapest[setV] = edge + } + } else { + cheapest[setV] = edge + } + } + } + + for (_, edge) in cheapest { + let u = edge.source + let v = edge.destination + let setU = uf.find(u) + let setV = uf.find(v) + + if setU != setV { + mst.append(edge) + uf.union(u, v) + numComponents -= 1 + } + } + } + + return mst + } +} diff --git a/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Kruskal.swift b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Kruskal.swift new file mode 100644 index 0000000..b4e0f2c --- /dev/null +++ b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Kruskal.swift @@ -0,0 +1,81 @@ +extension MinimumSpanningTreeAlgorithm { + /// Creates a Kruskal algorithm instance. + /// - Returns: An instance of `KruskalAlgorithm`. + @inlinable public static func kruskal() -> Self where Self == KruskalAlgorithm { + KruskalAlgorithm() + } +} + +/// An implementation of the Kruskal algorithm for finding the minimum spanning tree in a graph. +public struct KruskalAlgorithm: MinimumSpanningTreeAlgorithm { + /// Initializes a new `KruskalAlgorithm` instance. + @inlinable public init() {} + + /// Finds the minimum spanning tree in the graph using the Kruskal algorithm. + /// - Parameter graph: The graph in which to find the minimum spanning tree. + /// - Returns: An array of `GraphEdge` instances representing the edges in the minimum spanning tree. + @inlinable public func minimumSpanningTree(in graph: some WholeGraphProtocol) -> [GraphEdge] { + let sortedEdges = graph.allEdges.sorted { $0.value.weight < $1.value.weight } + let nodeCount = graph.allNodes.count + + var mst: [GraphEdge] = [] + var uf = UnionFind() + for edge in sortedEdges { + let source = edge.source + let destination = edge.destination + + if uf.find(source) != uf.find(destination) { + uf.union(source, destination) + mst.append(edge) + } + + if mst.count == nodeCount - 1 { + break + } + } + return mst + } +} + +/// A data structure for union-find (disjoint-set) operations. +public struct UnionFind { + /// A dictionary mapping each node to its parent node. + @usableFromInline var parent: [Node: Node] + + /// Initializes a new `UnionFind` instance. + @inlinable public init() { + self.parent = [:] + } + + /// Finds the representative (root) of the set containing the given node. + /// - Parameter node: The node for which to find the representative. + /// - Returns: The representative of the set containing the node. + @inlinable public mutating func find(_ node: Node) -> Node { + if parent[node] != node && parent[node] != nil { + parent[node] = find(parent[node]!) // Path compression + } else if parent[node] == nil { + parent[node] = node + } + return parent[node]! + } + + /// Unites the sets containing the two given nodes. + /// - Parameters: + /// - node1: The first node. + /// - node2: The second node. + @inlinable public mutating func union(_ node1: Node, _ node2: Node) { + let root1 = find(node1) + let root2 = find(node2) + if root1 != root2 { + parent[root2] = root1 + } + } + + /// Adds a node to the Union-Find data structure. + /// - Parameter node: The node to add. + @inlinable public mutating func add(_ node: Node) { + if parent[node] == nil { + parent[node] = node + } + } +} diff --git a/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Prim.swift b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Prim.swift new file mode 100644 index 0000000..b10494e --- /dev/null +++ b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree+Prim.swift @@ -0,0 +1,53 @@ +import Collections + +extension MinimumSpanningTreeAlgorithm { + /// Creates a Prim algorithm instance. + /// - Returns: An instance of `PrimAlgorithm`. + @inlinable public static func prim() -> Self where Self == PrimAlgorithm { + PrimAlgorithm() + } +} + +/// An implementation of the Prim algorithm for finding the minimum spanning tree in a graph. +public struct PrimAlgorithm: MinimumSpanningTreeAlgorithm { + /// Initializes a new `PrimAlgorithm` instance. + @inlinable public init() {} + + /// Finds the minimum spanning tree in the graph using the Prim algorithm. + /// - Parameter graph: The graph in which to find the minimum spanning tree. + /// - Returns: An array of `GraphEdge` instances representing the edges in the minimum spanning tree. + @inlinable public func minimumSpanningTree(in graph: some WholeGraphProtocol) -> [GraphEdge] { + guard let startNode = graph.allNodes.first else { + return [] + } + let nodeCount = graph.allNodes.count + + var mst: [GraphEdge] = [] + var visited: Set = [startNode] + var heap = Heap>() + + for edge in graph.edges(from: startNode) { + heap.insert(edge) + } + + while let edge = heap.popMin() { + let destination = edge.destination + if !visited.contains(destination) { + visited.insert(destination) + mst.append(edge) + + for nextEdge in graph.edges(from: destination) { + if !visited.contains(nextEdge.destination) { + heap.insert(nextEdge) + } + } + + if visited.count == nodeCount { + break + } + } + } + + return mst + } +} diff --git a/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree.swift b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree.swift new file mode 100644 index 0000000..fa1200e --- /dev/null +++ b/Sources/Graph/MinimumSpanningTree/Graph+MinimumSpanningTree.swift @@ -0,0 +1,23 @@ +extension WholeGraphProtocol where Edge: Weighted, Node: Hashable { + /// Finds the minimum spanning tree of the graph using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the minimum spanning tree. + /// - Returns: An array of `GraphEdge` instances representing the edges in the minimum spanning tree. + @inlinable public func minimumSpanningTree( + using algorithm: Algorithm + ) -> [GraphEdge] where Algorithm.Node == Node, Algorithm.Edge == Edge { + algorithm.minimumSpanningTree(in: self) + } +} + +/// A protocol that defines the requirements for a minimum spanning tree algorithm. +public protocol MinimumSpanningTreeAlgorithm { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph, which must conform to the `Weighted` protocol. + associatedtype Edge: Weighted + + /// Finds the minimum spanning tree in the graph. + /// - Parameter graph: The graph in which to find the minimum spanning tree. + /// - Returns: An array of `GraphEdge` instances representing the edges in the minimum spanning tree. + func minimumSpanningTree(in graph: some WholeGraphProtocol) -> [GraphEdge] +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPath+AStar.swift b/Sources/Graph/ShortestPath/Graph+ShortestPath+AStar.swift new file mode 100644 index 0000000..490ec19 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPath+AStar.swift @@ -0,0 +1,186 @@ +import Collections + +extension ShortestPathAlgorithm { + /// Creates an A* algorithm instance with a custom heuristic and cost calculation. + /// - Parameters: + /// - heuristic: The heuristic function to estimate the distance from a node to the goal. + /// - calculateTotalCost: A closure to calculate the total cost given the edge weight and distance. + /// - Returns: An instance of `AStarAlgorithm`. + @inlinable public static func aStar( + heuristic: Self.Heuristic, + calculateTotalCost: @escaping (Edge.Weight, Distance) -> Cost + ) -> Self where Self == AStarAlgorithm { + .init(heuristic: heuristic, calculateTotalCost: calculateTotalCost) + } + + /// Creates an A* algorithm instance with a custom heuristic and default cost calculation. + /// - Parameter heuristic: The heuristic function to estimate the distance from a node to the goal. + /// - Returns: An instance of `AStarAlgorithm`. + @inlinable public static func aStar( + heuristic: Self.Heuristic + ) -> Self where Self == AStarAlgorithm { + .init(heuristic: heuristic, calculateTotalCost: +) + } + + /// Creates an A* algorithm instance with a custom heuristic for floating-point distances. + /// - Parameter heuristic: The heuristic function to estimate the distance from a node to the goal. + /// - Returns: An instance of `AStarAlgorithm`. + @inlinable public static func aStar( + heuristic: Self.Heuristic + ) -> Self where Self == AStarAlgorithm, Edge.Weight: BinaryInteger { + .init(heuristic: heuristic) { Distance($0) + $1 } + } +} + +extension AStarAlgorithm.Heuristic where HScore: FloatingPoint, HScore.Magnitude == HScore { + /// Creates a heuristic based on the Euclidean distance. + /// - Parameter value: A closure to extract the coordinates from a node. + /// - Returns: An instance of `AStarAlgorithm.Heuristic`. + @inlinable public static func euclideanDistance( + of value: @escaping (Node) -> Coordinate + ) -> Self where HScore == Coordinate.Scalar { + self.init(distanceAlgorithm: .euclideanDistance(of: value)) + } + + /// Creates a heuristic based on the Manhattan distance. + /// - Parameter value: A closure to extract the coordinates from a node. + /// - Returns: An instance of `AStarAlgorithm.Heuristic`. + @inlinable public static func manhattanDistance( + of value: @escaping (Node) -> Coordinate + ) -> Self where HScore == Coordinate.Scalar { + self.init(distanceAlgorithm: .manhattanDistance(of: value)) + } +} + +/// An implementation of the A* algorithm for finding the shortest path in a graph. +public struct AStarAlgorithm: ShortestPathAlgorithm where Edge.Weight: Numeric { + /// A heuristic function used by the A* algorithm. + public struct Heuristic { + /// A closure to estimate the distance between two nodes. + public let estimatedDistance: (Node, Node) -> HScore + + /// Initializes a new heuristic with a custom distance estimation function. + /// - Parameter estimatedDistance: A closure to estimate the distance between two nodes. + @inlinable public init(estimatedDistance: @escaping (Node, Node) -> HScore) { + self.estimatedDistance = estimatedDistance + } + + /// Initializes a new heuristic with a distance algorithm. + /// - Parameter distanceAlgorithm: A distance algorithm to estimate the distance between two nodes. + @inlinable public init(distanceAlgorithm: DistanceAlgorithm) where HScore.Magnitude == HScore { + self.init(estimatedDistance: distanceAlgorithm.distance) + } + } + + /// The type of the g-score, which represents the cost from the start node to a given node. + public typealias GScore = Edge.Weight + + /// An implementation of the A* algorithm for finding the shortest path in a graph. + @usableFromInline let heuristic: Heuristic + /// A closure to calculate the total cost given the edge weight and distance. + @usableFromInline let calculateTotalCost: (GScore, HScore) -> FScore + + /// Initializes a new A* algorithm instance with a custom heuristic and cost calculation. + /// - Parameters: + /// - heuristic: The heuristic function to estimate the distance from a node to the goal. + /// - calculateTotalCost: A closure to calculate the total cost given the edge weight and distance. + @inlinable public init( + heuristic: Heuristic, + calculateTotalCost: @escaping (Edge.Weight, HScore) -> FScore + ) { + self.heuristic = heuristic + self.calculateTotalCost = calculateTotalCost + } + + /// Finds the shortest path from the source node to the destination node in the graph. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the shortest path. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable public func shortestPath( + from source: Node, + to destination: Node, + in graph: some GraphProtocol + ) -> Path? { + var openSet = Heap() + var costs: [Node: GScore] = [source: .zero] + var connectingEdges: [Node: GraphEdge] = [:] + var closedSet: Set = [] + + openSet.insert( + State( + node: source, + costSoFar: .zero, + estimatedTotalCost: calculateTotalCost(.zero, heuristic.estimatedDistance(source, destination)) + ) + ) + + while let currentState = openSet.popMin() { + let currentNode = currentState.node + + if currentNode == destination { + return Path(connectingEdges: connectingEdges, source: source, destination: destination) + } + + if !closedSet.insert(currentNode).inserted { + continue + } + + for edge in graph.edges(from: currentNode) { + let neighbor = edge.destination + let weight: Edge.Weight = edge.value.weight + let newCost: GScore = currentState.costSoFar + weight + + if costs[neighbor] == nil || newCost < costs[neighbor]! { + costs[neighbor] = newCost + connectingEdges[neighbor] = edge + let estimatedTotalCost = calculateTotalCost(newCost, heuristic.estimatedDistance(neighbor, destination)) + openSet.insert(State(node: neighbor, costSoFar: newCost, estimatedTotalCost: estimatedTotalCost)) + } + } + } + + return nil + } + + /// Represents a state in the A* algorithm, including the current node, the cost so far, and the estimated total cost. + @usableFromInline struct State: Comparable { + /// The current node in the state. + @usableFromInline let node: Node + /// The cost from the start node to the current node. + @usableFromInline let costSoFar: GScore + /// The estimated total cost from the start node to the goal node through the current node. + @usableFromInline let estimatedTotalCost: FScore + + /// Initializes a new state with the given node, cost so far, and estimated total cost. + /// - Parameters: + /// - node: The current node. + /// - costSoFar: The cost from the start node to the current node. + /// - estimatedTotalCost: The estimated total cost from the start node to the goal node through the current node. + @inlinable init(node: Node, costSoFar: GScore, estimatedTotalCost: FScore) { + self.node = node + self.costSoFar = costSoFar + self.estimatedTotalCost = estimatedTotalCost + } + + /// Compares two states based on their estimated total cost. + /// - Parameters: + /// - lhs: The left-hand side state. + /// - rhs: The right-hand side state. + /// - Returns: `true` if the estimated total cost of the left-hand side state is less than that of the right-hand side state. + @inlinable public static func < (lhs: State, rhs: State) -> Bool { + lhs.estimatedTotalCost < rhs.estimatedTotalCost + } + + /// Checks if two states are equal based on their node and estimated total cost. + /// - Parameters: + /// - lhs: The left-hand side state. + /// - rhs: The right-hand side state. + /// - Returns: `true` if the node and estimated total cost of both states are equal. + @inlinable public static func == (lhs: State, rhs: State) -> Bool { + lhs.node == rhs.node && + lhs.estimatedTotalCost == rhs.estimatedTotalCost + } + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPath+Dijkstra.swift b/Sources/Graph/ShortestPath/Graph+ShortestPath+Dijkstra.swift new file mode 100644 index 0000000..a8ca161 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPath+Dijkstra.swift @@ -0,0 +1,143 @@ +import Collections + +extension ShortestPathAlgorithm { + /// Creates a Dijkstra algorithm instance. + /// - Returns: An instance of `DijkstraAlgorithm`. + @inlinable public static func dijkstra() -> Self where Self == DijkstraAlgorithm { + .init() + } +} + +extension GraphProtocol where Node: Hashable, Edge: Weighted, Edge.Weight: Numeric, Edge.Weight.Magnitude == Edge.Weight { + /// Finds the shortest path from the source node to the destination node using the Dijkstra algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable public func shortestPath( + from source: Node, + to destination: Node + ) -> Path? { + shortestPath(from: source, to: destination, using: .dijkstra()) + } +} + +extension WeightedGraph where Edge.Weight: Numeric, Edge.Weight.Magnitude == Edge.Weight { + /// Finds the shortest path from the source to the destination using Dijkstra's algorithm. + /// - Parameters: + /// - source: The starting position in the grid. + /// - destination: The ending position in the grid. + /// - Returns: The shortest path from the source to the destination, or `nil` if no path exists. + @inlinable public func shortestPath( + from source: GridPosition, + to destination: GridPosition + ) -> Path? where Graph == GridGraph { + self.shortestPath(from: source, to: destination, using: .dijkstra()) + } + + /// Finds the shortest paths from the source to all other nodes using Dijkstra's algorithm. + /// - Parameters: + /// - source: The starting position in the grid. + /// - Returns: A dictionary mapping each node to its shortest path from the source. + @inlinable public func shortestPaths( + from source: GridPosition + ) -> [Node: Path] where Graph == GridGraph { + self.shortestPaths(from: source, using: .dijkstra()) + } +} + +/// An implementation of the Dijkstra algorithm for finding the shortest path in a graph. +public struct DijkstraAlgorithm: ShortestPathAlgorithm where Edge.Weight: Numeric, Edge.Weight.Magnitude == Edge.Weight { + /// Initializes a new `DijkstraAlgorithm` instance. + @inlinable public init() {} + + /// Finds the shortest path in the graph from the start node to the goal node using the Dijkstra algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the shortest path. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable public func shortestPath( + from source: Node, + to destination: Node, + in graph: some GraphProtocol + ) -> Path? { + let result = computeShortestPaths(from: source, stopAt: destination, in: graph) + return result.connectingEdges[destination].flatMap { _ in + Path(connectingEdges: result.connectingEdges, source: source, destination: destination) + } + } + + /// Computes the shortest paths from the source node to all other nodes in the graph using the Dijkstra algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: An optional target node to stop the computation early. + /// - graph: The graph in which to compute the shortest paths. + /// - Returns: A tuple containing the costs and connecting edges for the shortest paths. + @usableFromInline func computeShortestPaths( + from source: Node, + stopAt destination: Node?, + in graph: some GraphProtocol + ) -> (costs: [Node: Edge.Weight], connectingEdges: [Node: GraphEdge]) { + var openSet = Heap() + var costs: [Node: Edge.Weight] = [source: .zero] + var connectingEdges: [Node: GraphEdge] = [:] + var closedSet: Set = [] + + openSet.insert( + State( + node: source, + totalCost: .zero + ) + ) + + while let currentState = openSet.popMin() { + let currentNode = currentState.node + + if let destination = destination, currentNode == destination { + break + } + + if !closedSet.insert(currentNode).inserted { + continue + } + + for edge in graph.edges(from: currentNode) { + let neighbor = edge.destination + let weight = edge.value.weight + let newCost = currentState.totalCost + weight + + if costs[neighbor] == nil || newCost < costs[neighbor]! { + costs[neighbor] = newCost + connectingEdges[neighbor] = edge + openSet.insert(State(node: neighbor, totalCost: newCost)) + } + } + } + + return (costs, connectingEdges) + } + + /// A structure representing the state of a node during the Dijkstra search. + @usableFromInline struct State: Comparable { + /// The node being evaluated. + @usableFromInline let node: Node + /// The total cost of the path to the node. + @usableFromInline let totalCost: Edge.Weight + + /// Initializes a new `State` instance with the given node and total cost. + @inlinable init(node: Node, totalCost: Edge.Weight) { + self.node = node + self.totalCost = totalCost + } + + /// Compares two states based on their costs. + /// - Parameters: + /// - lhs: The left-hand side state. + /// - rhs: The right-hand side state. + /// - Returns: `true` if the cost of the left-hand side state is less than that of the right-hand side state, `false` otherwise. + @inlinable public static func < (lhs: State, rhs: State) -> Bool { + lhs.totalCost < rhs.totalCost + } + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPath.swift b/Sources/Graph/ShortestPath/Graph+ShortestPath.swift new file mode 100644 index 0000000..9117056 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPath.swift @@ -0,0 +1,97 @@ +extension GraphProtocol where Node: Hashable, Edge: Weighted { + /// Finds the shortest path from the source node to the destination node using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - algorithm: The algorithm to use for finding the shortest path. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable public func shortestPath( + from source: Node, + to destination: Node, + using algorithm: some ShortestPathAlgorithm + ) -> Path? { + algorithm.shortestPath(from: source, to: destination, in: self) + } +} + +/// A protocol that defines the requirements for a shortest path algorithm. +public protocol ShortestPathAlgorithm { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the shortest path in the graph from the start node to the goal node. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the shortest path. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable func shortestPath( + from source: Node, + to destination: Node, + in graph: some GraphProtocol + ) -> Path? +} + +/// A structure representing a path in a graph. +public struct Path { + /// The source node of the path. + public let source: Node + /// The destination node of the path. + public let destination: Node + /// The edges that make up the path. + public let edges: [GraphEdge] + + /// Initializes a new `Path` instance with the given source, destination, and edges. + /// - Parameters: + /// - source: The source node of the path. + /// - destination: The destination node of the path. + /// - edges: The edges that make up the path. + @inlinable public init(source: Node, destination: Node, edges: [GraphEdge]) { + self.source = source + self.destination = destination + self.edges = edges + } + + /// The sequence of nodes in the path. + @inlinable public var path: [Node] { + [source] + edges.map(\.destination) + } +} + +extension Path: Equatable where Node: Equatable, Edge: Equatable {} +extension Path: Hashable where Node: Hashable, Edge: Hashable {} + +extension Path where Edge: Weighted, Edge.Weight: Numeric { + /// The total cost of the path, calculated as the sum of the weights of the edges. + @inlinable public var cost: Edge.Weight { + edges.lazy.map(\.value.weight).reduce(into: .zero, +=) + } +} + +extension Path where Node: Hashable { + /// Initializes a path from a dictionary of connecting edges, a source node, and a destination node. + /// - Parameters: + /// - connectingEdges: A dictionary mapping nodes to their connecting edges. + /// - source: The source node of the path. + /// - destination: The destination node of the path. + @inlinable init?( + connectingEdges: [Node: GraphEdge], + source: Node, + destination: Node + ) { + var path: [GraphEdge] = [] + var currentNode = destination + + while currentNode != source { + guard let edge = connectingEdges[currentNode] else { + return nil + } + path.append(edge) + currentNode = edge.source + } + + self.init(source: source, destination: destination, edges: path.reversed()) + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathOnFullGraph+BellmanFord.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathOnFullGraph+BellmanFord.swift new file mode 100644 index 0000000..8175b9d --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathOnFullGraph+BellmanFord.swift @@ -0,0 +1,80 @@ +extension ShortestPathOnWholeGraphAlgorithm { + /// Creates a Bellman-Ford algorithm instance. + /// - Parameter max: The maximum weight of an edge in the graph. + /// - Returns: An instance of `BellmanFordAlgorithm`. + @inlinable public static func bellmanFord(max: Edge.Weight) -> Self where Self == BellmanFordAlgorithm { + .init(max: max) + } + + /// Creates a Bellman-Ford algorithm instance. + /// - Returns: An instance of `BellmanFordAlgorithm`. + @inlinable public static func bellmanFord() -> Self where Self == BellmanFordAlgorithm, Edge.Weight: FixedWidthInteger { + .init(max: .max) + } +} + +/// An implementation of the Bellman-Ford algorithm for finding the shortest path in a graph. +public struct BellmanFordAlgorithm: ShortestPathOnWholeGraphAlgorithm where Edge.Weight: Numeric { + /// The maximum value possible for weight. + @usableFromInline let max: Edge.Weight + + /// Initializes a new `BellmanFordAlgorithm` instance. + /// - Parameter max: The maximum value possible for weight. + @inlinable public init(max: Edge.Weight) { + self.max = max + } + + /// Finds the shortest path in the graph from the start node to the goal node using the Bellman-Ford algorithm. + /// - Parameters: + /// - source: The starting node. + /// - destination: The target node. + /// - graph: The graph in which to find the shortest path. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable public func shortestPath( + from source: Node, + to destination: Node, + in graph: some WholeGraphProtocol + ) -> Path? { + let result = computeShortestPaths(from: source, in: graph) + return Path(connectingEdges: result.predecessors, source: source, destination: destination) + } + + /// Computes the shortest paths from the source node to all other nodes in the graph using the Bellman-Ford algorithm. + @usableFromInline func computeShortestPaths( + from source: Node, + in graph: some WholeGraphProtocol + ) -> (distances: [Node: Edge.Weight], predecessors: [Node: GraphEdge]) { + var distances: [Node: Edge.Weight] = [:] + var predecessors: [Node: GraphEdge] = [:] + + for node in graph.allNodes { + distances[node] = max + } + distances[source] = .zero + + for _ in 1.. + ) -> Path? { + algorithm.shortestPath(from: source, to: destination, in: self) + } +} + +/// A protocol that defines the requirements for a shortest path algorithm on a whole graph. +public protocol ShortestPathOnWholeGraphAlgorithm { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the shortest path in the graph from the start node to the goal node. + /// - Parameters: + /// - graph: The graph in which to find the shortest path. + /// - start: The starting node. + /// - goal: The target node. + /// - Returns: A `Path` instance representing the shortest path, or `nil` if no path is found. + @inlinable func shortestPath( + from start: Node, + to goal: Node, + in graph: some WholeGraphProtocol + ) -> Path? +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPaths+Dijkstra.swift b/Sources/Graph/ShortestPath/Graph+ShortestPaths+Dijkstra.swift new file mode 100644 index 0000000..8cf16fa --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPaths+Dijkstra.swift @@ -0,0 +1,43 @@ +extension ShortestPathsAlgorithm { + /// Creates a Dijkstra algorithm instance. + /// - Returns: An instance of `DijkstraAlgorithm`. + @inlinable public static func dijkstra() -> Self where Self == DijkstraAlgorithm { + .init() + } +} + +extension GraphProtocol where Node: Hashable, Edge: Weighted, Edge.Weight: Numeric, Edge.Weight.Magnitude == Edge.Weight { + /// Finds the shortest paths from the source node to all other nodes using the Dijkstra algorithm. + /// - Parameter source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths( + from source: Node + ) -> [Node: Path] { + shortestPaths(from: source, using: .dijkstra()) + } +} + +extension DijkstraAlgorithm: ShortestPathsAlgorithm { + /// Finds the shortest paths in the graph from the start node to all other nodes using the Dijkstra algorithm. + /// - Parameters: + /// - graph: The graph in which to find the shortest paths. + /// - source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths( + from source: Node, + in graph: some GraphProtocol + ) -> [Node: Path] { + let result = computeShortestPaths(from: source, stopAt: nil, in: graph) + + var paths: [Node: Path] = [:] + for node in result.connectingEdges.keys { + if let path = Path(connectingEdges: result.connectingEdges, source: source, destination: node) { + paths[node] = path + } + } + + paths[source] = Path(source: source, destination: source, edges: []) + + return paths + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPaths.swift b/Sources/Graph/ShortestPath/Graph+ShortestPaths.swift new file mode 100644 index 0000000..4ff2b59 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPaths.swift @@ -0,0 +1,31 @@ +extension GraphProtocol { + /// Finds the shortest paths from the source node to all other nodes using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the shortest paths. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths>( + from source: Node, + using algorithm: Algorithm + ) -> [Node: Path] { + algorithm.shortestPaths(from: source, in: self) + } +} + +/// A protocol that defines the requirements for a shortest paths algorithm. +public protocol ShortestPathsAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the shortest paths in the graph from the start node to all other nodes. + /// - Parameters: + /// - graph: The graph in which to find the shortest paths. + /// - source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable func shortestPaths( + from source: Node, + in graph: some GraphProtocol + ) -> [Node: Path] +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+FloydWarshall.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+FloydWarshall.swift new file mode 100644 index 0000000..aede6a8 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+FloydWarshall.swift @@ -0,0 +1,70 @@ +extension ShortestPathsForAllPairsAlgorithm { + /// Creates a Floyd-Warshall algorithm instance. + /// - Returns: An instance of `FloydWarshallAlgorithm`. + @inlinable public static func floydWarshall() -> Self where Self == FloydWarshallAlgorithm, Edge.Weight: FixedWidthInteger { + .init(max: .max) + } + + /// Creates a Floyd-Warshall algorithm instance. + /// - Parameter max: The maximum weight of an edge in the graph. + /// - Returns: An instance of `FloydWarshallAlgorithm`. + @inlinable public static func floydWarshall(max: Edge.Weight) -> Self where Self == FloydWarshallAlgorithm { + .init(max: max) + } +} + +extension WholeGraphProtocol where Node: Hashable, Edge: Weighted, Edge.Weight: FixedWidthInteger { + /// Finds the shortest paths between all pairs of nodes using the Floyd-Warshall algorithm. + @inlinable public func shortestPathsForAllPairs() -> [Node: [Node: Edge.Weight]] { + shortestPathsForAllPairs(using: .floydWarshall()) + } +} + +/// An implementation of the Floyd-Warshall algorithm for finding the shortest paths between all pairs of nodes in a graph. +public struct FloydWarshallAlgorithm: ShortestPathsForAllPairsAlgorithm where Edge.Weight: Numeric { + /// The maximum value possible for weight. + @usableFromInline let max: Edge.Weight + + /// Initializes a new `FloydWarshallAlgorithm` instance. + @inlinable public init(max: Edge.Weight) { + self.max = max + } + + /// Computes the shortest paths between all pairs of nodes in the graph using the Floyd-Warshall algorithm. + @inlinable public func shortestPathsForAllPairs(in graph: some WholeGraphProtocol) -> [Node: [Node: Edge.Weight]] { + var distances: [Node: [Node: Edge.Weight]] = [:] + + // Initialize distances + for u in graph.allNodes { + distances[u] = [:] + for v in graph.allNodes { + if u == v { + distances[u]?[v] = .zero + } else { + distances[u]?[v] = nil + } + } + } + + for edge in graph.allEdges { + distances[edge.source]?[edge.destination] = edge.value.weight + } + + // Floyd-Warshall algorithm + for k in graph.allNodes { + for i in graph.allNodes { + for j in graph.allNodes { + if let ik = distances[i]?[k], let kj = distances[k]?[j] { + let ij = distances[i]?[j] ?? max + let newDistance = ik + kj + if newDistance < ij { + distances[i]?[j] = newDistance + } + } + } + } + } + + return distances + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+Johnson.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+Johnson.swift new file mode 100644 index 0000000..9f1072f --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs+Johnson.swift @@ -0,0 +1,98 @@ +extension ShortestPathsForAllPairsAlgorithm { + /// Creates a new Johnson's algorithm instance with the specified edge weight function. + /// - Parameter edge: A function that returns an edge with the specified weight. + /// - Returns: An instance of `JohnsonAlgorithm`. + @inlinable public static func johnson(edge: @escaping (Edge.Weight) -> Edge) -> Self where Self == JohnsonAlgorithm { + .init(edge: edge) + } +} + +/// An implementation of Johnson's algorithm for finding the shortest paths between all pairs of nodes in a graph. +public struct JohnsonAlgorithm: ShortestPathsForAllPairsAlgorithm where Edge.Weight: FixedWidthInteger, Edge.Weight.Magnitude == Edge.Weight { + /// A function that returns an edge with the specified weight. + @usableFromInline let edge: (Edge.Weight) -> Edge + + /// Initializes a new `JohnsonAlgorithm` instance with the specified edge weight function. + @inlinable public init(edge: @escaping (Edge.Weight) -> Edge) { + self.edge = edge + } + + /// Computes the shortest paths between all pairs of nodes in the graph using Johnson's algorithm. + @usableFromInline enum JohnsonNode: Hashable & Comparable { + case original(Node) + case q + + @inlinable static func < (lhs: JohnsonNode, rhs: JohnsonNode) -> Bool { + switch (lhs, rhs) { + case (.original(let a), .original(let b)): + return a < b + case (.original, .q): + return true + case (.q, .original): + return false + case (.q, .q): + return false + } + } + } + + /// Computes the shortest paths between all pairs of nodes in the graph using Johnson's algorithm. + @inlinable public func shortestPathsForAllPairs(in graph: some WholeGraphProtocol) -> [Node: [Node: Edge.Weight]] { + // Step 1: Wrap nodes and create a unique node 'q' + let q = JohnsonNode.q + let originalNodes = graph.allNodes.map { JohnsonNode.original($0) } + + // Step 2: Create extended graph with edges from 'q' to all other nodes + var extendedEdges: [GraphEdge] = graph.allEdges.map { edge in + GraphEdge( + source: JohnsonNode.original(edge.source), + destination: JohnsonNode.original(edge.destination), + value: edge.value + ) + } + for node in originalNodes { + extendedEdges.append(GraphEdge(source: q, destination: node, value: edge(.zero))) + } + + // Step 3: Run Bellman-Ford from 'q' + let extendedGraph = Graph(edges: extendedEdges) + let bellmanFord = BellmanFordAlgorithm(max: Edge.Weight.max) + let bellmanFordResult = bellmanFord.computeShortestPaths(from: q, in: extendedGraph) + + if bellmanFordResult.distances.isEmpty { + return [:] // Negative cycle detected + } + let h = bellmanFordResult.distances + + // Step 4: Re-weight the edges + var reweightedEdges: [GraphEdge] = [] + for edge in extendedEdges { + // Skip edges originating from 'q' + if edge.source == q { continue } + let newWeight = edge.value.weight + h[edge.source]! - h[edge.destination]! + reweightedEdges.append(GraphEdge(source: edge.source, destination: edge.destination, value: self.edge(newWeight))) + } + + let reweightedGraph = Graph(edges: reweightedEdges) + + // Step 5: Run Dijkstra's algorithm from each node + var distances: [Node: [Node: Edge.Weight]] = [:] + let dijkstra = DijkstraAlgorithm() + + for node in originalNodes { + let result = dijkstra.computeShortestPaths(from: node, stopAt: nil, in: reweightedGraph) + var nodeDistances: [Node: Edge.Weight] = [:] + for (dest, dist) in result.costs { + // Exclude paths involving 'q' + if case .original(let originalDest) = dest { + nodeDistances[originalDest] = dist - h[node]! + h[dest]! + } + } + if case .original(let originalNode) = node { + distances[originalNode] = nodeDistances + } + } + + return distances + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs.swift new file mode 100644 index 0000000..3f85cb4 --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathsForAllPairs.swift @@ -0,0 +1,18 @@ +extension WholeGraphProtocol { + /// Computes the shortest paths from all nodes to all other nodes in the graph using the specified algorithm. + @inlinable public func shortestPathsForAllPairs>( + using algorithm: Algorithm + ) -> [Node: [Node: Edge.Weight]] { + algorithm.shortestPathsForAllPairs(in: self) + } +} + +/// A protocol that defines the requirements for an algorithm that computes the shortest paths for all pairs of nodes in a graph. +public protocol ShortestPathsForAllPairsAlgorithm { + associatedtype Node: Hashable + associatedtype Edge: Weighted where Edge.Weight: Numeric + + @inlinable func shortestPathsForAllPairs( + in graph: some WholeGraphProtocol + ) -> [Node: [Node: Edge.Weight]] +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph+BellmanFord.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph+BellmanFord.swift new file mode 100644 index 0000000..eafbd9b --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph+BellmanFord.swift @@ -0,0 +1,49 @@ +extension ShortestPathsOnWholeGraphAlgorithm { + /// Creates a Bellman-Ford algorithm instance. + /// - Returns: An instance of `BellmanFordAlgorithm`. + @inlinable public static func bellmanFord(max: Edge.Weight) -> Self where Self == BellmanFordAlgorithm { + .init(max: max) + } + + /// Creates a Bellman-Ford algorithm instance. + /// - Returns: An instance of `BellmanFordAlgorithm`. + @inlinable public static func bellmanFord() -> Self where Self == BellmanFordAlgorithm, Edge.Weight: FixedWidthInteger { + .init(max: .max) + } +} + +extension WholeGraphProtocol where Node: Hashable, Edge: Weighted, Edge.Weight: FixedWidthInteger { + /// Finds the shortest paths from the source node to all other nodes using the Bellman-Ford algorithm. + /// - Parameter source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths( + from source: Node + ) -> [Node: Path] { + shortestPaths(from: source, using: .bellmanFord()) + } +} + +extension BellmanFordAlgorithm: ShortestPathsOnWholeGraphAlgorithm { + /// Finds the shortest paths in the graph from the start node to all other nodes using the Bellman-Ford algorithm. + /// - Parameters: + /// - graph: The graph in which to find the shortest paths. + /// - source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths( + from source: Node, + in graph: some WholeGraphProtocol + ) -> [Node: Path] { + let result = computeShortestPaths(from: source, in: graph) + var paths: [Node: Path] = [:] + for node in graph.allNodes { + if node == source { + paths[node] = Path(source: source, destination: source, edges: []) + } else if let _ = result.predecessors[node] { + if let path = Path(connectingEdges: result.predecessors, source: source, destination: node) { + paths[node] = path + } + } + } + return paths + } +} diff --git a/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph.swift b/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph.swift new file mode 100644 index 0000000..a2c2a0b --- /dev/null +++ b/Sources/Graph/ShortestPath/Graph+ShortestPathsOnFullGraph.swift @@ -0,0 +1,31 @@ +extension WholeGraphProtocol { + /// Finds the shortest paths from the source node to all other nodes using the specified algorithm. + /// - Parameters: + /// - source: The starting node. + /// - algorithm: The algorithm to use for finding the shortest paths. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable public func shortestPaths>( + from source: Node, + using algorithm: Algorithm + ) -> [Node: Path] { + algorithm.shortestPaths(from: source, in: self) + } +} + +/// A protocol that defines the requirements for a shortest paths algorithm. +public protocol ShortestPathsOnWholeGraphAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the shortest paths in the graph from the start node to all other nodes. + /// - Parameters: + /// - graph: The graph in which to find the shortest paths. + /// - source: The starting node. + /// - Returns: A dictionary where the keys are the nodes and the values are the paths from the source node to the respective nodes. + @inlinable func shortestPaths( + from source: Node, + in graph: some WholeGraphProtocol + ) -> [Node: Path] +} diff --git a/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Kosaraju.swift b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Kosaraju.swift new file mode 100644 index 0000000..474e1b7 --- /dev/null +++ b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Kosaraju.swift @@ -0,0 +1,60 @@ +extension StronglyConnectedComponentsAlgorithm { + /// Creates a Kosaraju algorithm instance. + /// - Returns: An instance of `KosarajuSCCAlgorithm`. + @inlinable public static func kosaraju() -> Self where Self == KosarajuSCCAlgorithm { + .init() + } +} + +/// An implementation of the Kosaraju algorithm for finding strongly connected components in a graph. +public struct KosarajuSCCAlgorithm: StronglyConnectedComponentsAlgorithm { + /// Initializes a new `KosarajuSCCAlgorithm` instance. + @inlinable public init() {} + + /// Finds the strongly connected components in the graph using the Kosaraju algorithm. + /// - Parameter graph: The graph in which to find the strongly connected components. + /// - Returns: An array of arrays, where each inner array contains the nodes of a strongly connected component. + @inlinable public func findStronglyConnectedComponents(in graph: some WholeGraphProtocol) -> [[Node]] { + guard let sorted = graph.topologicalSort() else { return [] } + + let reversedGraph = TransposedGraph(base: graph) + + var result: [[Node]] = [] + + var visited: Set = [] + for node in sorted.reversed() { + if !visited.contains(node) { + var scc: [Node] = [] + dfsCollect(graph: reversedGraph, node: node, visited: &visited, scc: &scc) + result.append(scc) + } + } + + return result + } + + /// Performs a depth-first search to collect nodes in a strongly connected component. + /// - Parameters: + /// - graph: The graph being traversed. + /// - node: The starting node for the DFS. + /// - visited: A set of visited nodes. + /// - scc: An array to collect the nodes in the strongly connected component. + @usableFromInline func dfsCollect(graph: some WholeGraphProtocol, node: Node, visited: inout Set, scc: inout [Node]) { + visited.insert(node) + scc.append(node) + for edge in graph.edges(from: node) { + let neighbor = edge.destination + if !visited.contains(neighbor) { + dfsCollect(graph: graph, node: neighbor, visited: &visited, scc: &scc) + } + } + } + + /// Reverses the edges of the graph. + /// - Parameter graph: The graph to reverse. + /// - Returns: A new graph with all edges reversed. + @usableFromInline func reverseGraph(_ graph: some WholeGraphProtocol) -> Graph { + let reversedEdges = graph.allEdges.map { GraphEdge(source: $0.destination, destination: $0.source, value: $0.value) } + return Graph(edges: reversedEdges) + } +} diff --git a/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Tarjan.swift b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Tarjan.swift new file mode 100644 index 0000000..0bbb23a --- /dev/null +++ b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents+Tarjan.swift @@ -0,0 +1,78 @@ +extension StronglyConnectedComponentsAlgorithm { + /// Creates a Tarjan algorithm instance. + /// - Returns: An instance of `TarjanSCCAlgorithm`. + @inlinable public static func tarjan() -> Self where Self == TarjanSCCAlgorithm { + .init() + } +} + +/// An implementation of the Tarjan algorithm for finding strongly connected components in a graph. +public struct TarjanSCCAlgorithm: StronglyConnectedComponentsAlgorithm { + @inlinable public init() {} + + /// Finds the strongly connected components in the graph using the Tarjan algorithm. + /// - Parameter graph: The graph in which to find the strongly connected components. + /// - Returns: An array of arrays, where each inner array contains the nodes of a strongly connected component. + @inlinable public func findStronglyConnectedComponents(in graph: some WholeGraphProtocol) -> [[Node]] { + var index = 0 + var stack: [Node] = [] + var indices: [Node: Int] = [:] + var lowLinks: [Node: Int] = [:] + var onStack: Set = [] + var sccs: [[Node]] = [] + + for node in graph.allNodes where indices[node] == nil { + dfs(graph: graph, node: node, index: &index, stack: &stack, indices: &indices, lowLinks: &lowLinks, onStack: &onStack, sccs: &sccs) + } + + return sccs + } + + /// Performs a depth-first search to find strongly connected components. + /// - Parameters: + /// - graph: The graph being traversed. + /// - node: The starting node for the DFS. + /// - index: The current index in the DFS. + /// - stack: The stack of nodes being visited. + /// - indices: A dictionary mapping nodes to their indices. + /// - lowLinks: A dictionary mapping nodes to their low-link values. + /// - onStack: A set of nodes currently on the stack. + /// - sccs: An array to collect the strongly connected components. + @usableFromInline func dfs( + graph: some WholeGraphProtocol, + node: Node, + index: inout Int, + stack: inout [Node], + indices: inout [Node: Int], + lowLinks: inout [Node: Int], + onStack: inout Set, + sccs: inout [[Node]] + ) { + indices[node] = index + lowLinks[node] = index + index += 1 + stack.append(node) + onStack.insert(node) + + for edge in graph.edges(from: node) { + let neighbor = edge.destination + if indices[neighbor] == nil { + dfs(graph: graph, node: neighbor, index: &index, stack: &stack, indices: &indices, lowLinks: &lowLinks, onStack: &onStack, sccs: &sccs) + lowLinks[node] = min(lowLinks[node]!, lowLinks[neighbor]!) + } else if onStack.contains(neighbor) { + lowLinks[node] = min(lowLinks[node]!, indices[neighbor]!) + } + } + + if lowLinks[node] == indices[node] { + var scc: [Node] = [] + var poppedNode: Node + repeat { + poppedNode = stack.removeLast() + onStack.remove(poppedNode) + scc.append(poppedNode) + } while poppedNode != node + sccs.append(scc) + } + } +} diff --git a/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents.swift b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents.swift new file mode 100644 index 0000000..135431e --- /dev/null +++ b/Sources/Graph/StronglyConnectedComponents/Graph+StronglyConnectedComponents.swift @@ -0,0 +1,23 @@ +extension WholeGraphProtocol { + /// Finds the strongly connected components in the graph using the specified algorithm. + /// - Parameter algorithm: The algorithm to use for finding the strongly connected components. + /// - Returns: An array of arrays, where each inner array contains the nodes of a strongly connected component. + @inlinable public func findStronglyConnectedComponents>( + using algorithm: Algorithm + ) -> [[Node]] { + algorithm.findStronglyConnectedComponents(in: self) + } +} + +/// A protocol that defines the requirements for a strongly connected components algorithm. +public protocol StronglyConnectedComponentsAlgorithm { + /// The type of nodes in the graph. + associatedtype Node: Hashable + /// The type of edges in the graph. + associatedtype Edge + + /// Finds the strongly connected components in the graph. + /// - Parameter graph: The graph in which to find the strongly connected components. + /// - Returns: An array of arrays, where each inner array contains the nodes of a strongly connected component. + func findStronglyConnectedComponents(in graph: some WholeGraphProtocol) -> [[Node]] +} diff --git a/Sources/Graph/Traversal/BinaryGraph+Search.swift b/Sources/Graph/Traversal/BinaryGraph+Search.swift new file mode 100644 index 0000000..0801ecd --- /dev/null +++ b/Sources/Graph/Traversal/BinaryGraph+Search.swift @@ -0,0 +1,21 @@ +extension BinaryGraphProtocol { + /// Searches for the first visit that meets the goal criteria using the specified traversal strategy. + /// - Parameters: + /// - node: The starting node for the search. + /// - strategy: The traversal strategy to use for the search. + /// - goal: A closure that takes a visit and returns a Boolean value indicating whether the visit meets the goal criteria. + /// - Returns: The first visit that meets the goal criteria, or `nil` if no such visit is found. + @inlinable public func searchFirst(from node: Node, strategy: some BinaryGraphTraversalStrategy, goal: (Visit) -> Bool) -> Visit? { + traversal(from: node, strategy: strategy).first(where: goal) + } + + /// Searches for all visits that meet the goal criteria using the specified traversal strategy. + /// - Parameters: + /// - node: The starting node for the search. + /// - strategy: The traversal strategy to use for the search. + /// - goal: A closure that takes a visit and returns a Boolean value indicating whether the visit meets the goal criteria. + /// - Returns: An array of visits that meet the goal criteria. + @inlinable public func searchAll(from node: Node, strategy: some BinaryGraphTraversalStrategy, goal: (Visit) -> Bool) -> [Visit] { + traversal(from: node, strategy: strategy).filter(goal) + } +} diff --git a/Sources/Graph/Traversal/BinaryGraph+Traversal.swift b/Sources/Graph/Traversal/BinaryGraph+Traversal.swift new file mode 100644 index 0000000..4056eca --- /dev/null +++ b/Sources/Graph/Traversal/BinaryGraph+Traversal.swift @@ -0,0 +1,76 @@ +extension BinaryGraphProtocol { + /// Creates a traversal sequence from the specified node using the given traversal strategy. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: A `BinaryGraphTraversal` instance representing the traversal sequence. + @inlinable public func traversal>(from node: Node, strategy: Strategy) -> BinaryGraphTraversal { + .init(graph: self, startNode: node, strategy: strategy) + } + + /// Traverses the graph from the specified node using the given traversal strategy and returns an array of visits. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: An array of visits resulting from the traversal. + @inlinable public func traverse(from node: Node, strategy: some BinaryGraphTraversalStrategy) -> [Visit] { + Array(traversal(from: node, strategy: strategy)) + } +} + +/// A sequence representing the traversal of a binary graph using a specified strategy. +public struct BinaryGraphTraversal: Sequence where Graph.Node == Strategy.Node, Graph.Edge == Strategy.Edge { + typealias Node = Graph.Node + typealias Edge = Graph.Edge + + /// The graph being traversed. + @usableFromInline let graph: Graph + /// The starting node for the traversal. + @usableFromInline let startNode: Graph.Node + /// The strategy used for the traversal. + @usableFromInline var strategy: Strategy + + /// Initializes a new traversal sequence. + /// - Parameters: + /// - graph: The graph to traverse. + /// - startNode: The starting node for the traversal. + /// - strategy: The strategy to use for the traversal. + @inlinable public init(graph: Graph, startNode: Graph.Node, strategy: Strategy) { + self.graph = graph + self.startNode = startNode + self.strategy = strategy + } + + /// An iterator for the traversal sequence. + public struct Iterator: IteratorProtocol { + /// The graph being traversed. + @usableFromInline let graph: Graph + /// The storage used by the strategy during traversal. + @usableFromInline var storage: Strategy.Storage + /// The strategy used for the traversal. + @usableFromInline var strategy: Strategy + + /// Initializes a new iterator for the traversal sequence. + /// - Parameters: + /// - graph: The graph to traverse. + /// - startNode: The starting node for the traversal. + /// - strategy: The strategy to use for the traversal. + @inlinable public init(graph: Graph, startNode: Graph.Node, strategy: Strategy) { + self.graph = graph + self.strategy = strategy + self.storage = strategy.initializeStorage(startNode: startNode) + } + + /// Advances to the next element in the traversal sequence. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public mutating func next() -> Strategy.Visit? { + strategy.next(from: &storage, graph: graph) + } + } + + /// Creates an iterator for the traversal sequence. + /// - Returns: An iterator for the traversal sequence. + @inlinable public func makeIterator() -> Iterator { + Iterator(graph: graph, startNode: startNode, strategy: strategy) + } +} diff --git a/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy+DFS+Inorder.swift b/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy+DFS+Inorder.swift new file mode 100644 index 0000000..6a607eb --- /dev/null +++ b/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy+DFS+Inorder.swift @@ -0,0 +1,85 @@ +import Collections + +extension DFSOrder { + /// Creates an inorder depth-first search order. + /// - Returns: An instance of `DFSOrder` configured for inorder traversal. + @inlinable public static func inorder() -> Self where Self == DFSOrder> { + .init() + } +} + +extension BinaryGraphTraversalStrategy { + /// Creates a depth-first search strategy with the specified visitor and order. + /// - Parameters: + /// - visitor: The visitor to use during traversal. + /// - order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchInorder` configured with the specified visitor and order. + @inlinable public static func dfs( + _ visitor: Visitor, + order: DFSOrder> + ) -> Self where Self == DepthFirstSearchInorder { + .init(visitor: visitor) + } + + /// Creates a depth-first search strategy with the specified order. + /// - Parameter order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchInorder` configured with the specified order. + @inlinable public static func dfs( + order: DFSOrder>> + ) -> Self where Self == DepthFirstSearchInorder> { + .init(visitor: .onlyNodes()) + } +} + +/// A depth-first search strategy that performs an inorder traversal. +public struct DepthFirstSearchInorder: BinaryGraphTraversalStrategy { + public typealias Node = Visitor.Node + public typealias Edge = Visitor.Edge + public typealias Visit = Visitor.Visit + public typealias Storage = Deque<(isFirst: Bool, visit: Visit)> + + /// The visitor used during traversal. + @usableFromInline let visitor: Visitor + + /// Initializes a new inorder depth-first search strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + @inlinable public init(visitor: Visitor) { + self.visitor = visitor + } + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable public func initializeStorage(startNode: Node) -> Storage { + Deque([(isFirst: true, visit: visitor.visit(node: startNode, from: nil))]) + } + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - stack: The storage used by the strategy during traversal. + /// - graph: The graph being traversed. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from stack: inout Storage, graph: some BinaryGraphProtocol) -> Visitor.Visit? { + guard let (isFirst, visit) = stack.popLast() else { return nil } + if isFirst { + let edges = graph.edges(from: node(from: visit)) + if let edge = edges.rhs { + stack.append((isFirst: true, visit: visitor.visit(node: edge.destination, from: (visit, edge)))) + } + stack.append((isFirst: false, visit: visit)) + if let edge = edges.lhs { + stack.append((isFirst: true, visit: visitor.visit(node: edge.destination, from: (visit, edge)))) + } + return next(from: &stack, graph: graph) + } else { + return visit + } + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + visitor.node(from: visit) + } +} diff --git a/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy.swift b/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy.swift new file mode 100644 index 0000000..0fb6996 --- /dev/null +++ b/Sources/Graph/Traversal/BinaryGraph+TraversalStrategy.swift @@ -0,0 +1,23 @@ +/// A protocol defining a strategy for traversing a binary graph. +public protocol BinaryGraphTraversalStrategy { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. + associatedtype Edge + /// The type of visits produced during traversal. + associatedtype Visit + /// The type of storage used by the strategy during traversal. + associatedtype Storage + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable func initializeStorage(startNode: Node) -> Storage + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - storage: The storage used by the strategy during traversal. + /// - graph: The graph being traversed. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable func next(from storage: inout Storage, graph: some BinaryGraphProtocol) -> Visit? +} diff --git a/Sources/Graph/Traversal/Graph+Search.swift b/Sources/Graph/Traversal/Graph+Search.swift new file mode 100644 index 0000000..3bd8128 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+Search.swift @@ -0,0 +1,21 @@ +extension GraphProtocol { + /// Searches for the first visit that satisfies the given goal using the specified traversal strategy. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - goal: A closure that takes a visit and returns a Boolean value indicating whether the visit satisfies the goal. + /// - Returns: The first visit that satisfies the goal, or `nil` if no such visit is found. + @inlinable public func searchFirst(from node: Node, strategy: some GraphTraversalStrategy, goal: (Visit) -> Bool) -> Visit? { + traversal(from: node, strategy: strategy).first(where: goal) + } + + /// Searches for all visits that satisfy the given goal using the specified traversal strategy. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - goal: A closure that takes a visit and returns a Boolean value indicating whether the visit satisfies the goal. + /// - Returns: An array of visits that satisfy the goal. + @inlinable public func searchAll(from node: Node, strategy: some GraphTraversalStrategy, goal: (Visit) -> Bool) -> [Visit] { + traversal(from: node, strategy: strategy).filter(goal) + } +} diff --git a/Sources/Graph/Traversal/Graph+Traversal.swift b/Sources/Graph/Traversal/Graph+Traversal.swift new file mode 100644 index 0000000..73762d5 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+Traversal.swift @@ -0,0 +1,76 @@ +extension GraphProtocol { + /// Creates a traversal sequence from the specified node using the given traversal strategy. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: A `GraphTraversal` instance representing the traversal sequence. + @inlinable public func traversal>(from node: Node, strategy: Strategy) -> GraphTraversal { + .init(graph: self, startNode: node, strategy: strategy) + } + + /// Traverses the graph from the specified node using the given traversal strategy and returns an array of visits. + /// - Parameters: + /// - node: The starting node for the traversal. + /// - strategy: The traversal strategy to use. + /// - Returns: An array of visits resulting from the traversal. + @inlinable public func traverse(from node: Node, strategy: some GraphTraversalStrategy) -> [Visit] { + Array(traversal(from: node, strategy: strategy)) + } +} + +/// A sequence representing the traversal of a graph using a specified strategy. +public struct GraphTraversal: Sequence where Graph.Node == Strategy.Node, Graph.Edge == Strategy.Edge { + typealias Node = Graph.Node + typealias Edge = Graph.Edge + + /// The graph being traversed. + @usableFromInline let graph: Graph + /// The starting node for the traversal. + @usableFromInline let startNode: Graph.Node + /// The strategy used for the traversal. + @usableFromInline var strategy: Strategy + + /// Initializes a new traversal sequence. + /// - Parameters: + /// - graph: The graph to traverse. + /// - startNode: The starting node for the traversal. + /// - strategy: The strategy to use for the traversal. + @inlinable public init(graph: Graph, startNode: Graph.Node, strategy: Strategy) { + self.graph = graph + self.startNode = startNode + self.strategy = strategy + } + + /// An iterator for the traversal sequence. + public struct Iterator: IteratorProtocol { + /// The graph being traversed. + @usableFromInline let graph: Graph + /// The storage used by the strategy during traversal. + @usableFromInline var storage: Strategy.Storage + /// The strategy used for the traversal. + @usableFromInline var strategy: Strategy + + /// Initializes a new iterator for the traversal sequence. + /// - Parameters: + /// - graph: The graph to traverse. + /// - startNode: The starting node for the traversal. + /// - strategy: The strategy to use for the traversal. + @inlinable public init(graph: Graph, startNode: Graph.Node, strategy: Strategy) { + self.graph = graph + self.strategy = strategy + self.storage = strategy.initializeStorage(startNode: startNode) + } + + /// Advances to the next element in the traversal sequence. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public mutating func next() -> Strategy.Visit? { + strategy.next(from: &storage, graph: graph) + } + } + + /// Creates an iterator for the traversal sequence. + /// - Returns: An iterator for the traversal sequence. + @inlinable public func makeIterator() -> Iterator { + Iterator(graph: graph, startNode: startNode, strategy: strategy) + } +} diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy+BFS.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy+BFS.swift new file mode 100644 index 0000000..a6ad5e3 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy+BFS.swift @@ -0,0 +1,63 @@ +import Collections + +extension GraphTraversalStrategy { + /// Creates a breadth-first search traversal strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + /// - Returns: A `BreadthFirstSearch` instance configured with the specified visitor. + @inlinable public static func bfs( + _ visitor: Visitor + ) -> Self where Self == BreadthFirstSearch { + .init(visitor: visitor) + } + + /// Creates a breadth-first search traversal strategy with a default node visitor. + /// - Returns: A `BreadthFirstSearch` instance configured with a default node visitor. + @inlinable public static func bfs() -> Self where Self == BreadthFirstSearch> { + .init(visitor: .onlyNodes()) + } +} + +/// A breadth-first search traversal strategy. +public struct BreadthFirstSearch: GraphTraversalStrategy { + public typealias Storage = Deque + public typealias Node = Visitor.Node + public typealias Edge = Visitor.Edge + public typealias Visit = Visitor.Visit + + /// The visitor used during traversal. + @usableFromInline let visitor: Visitor + + /// Initializes a new breadth-first search traversal strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + @inlinable public init(visitor: Visitor) { + self.visitor = visitor + } + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable public func initializeStorage(startNode: Node) -> Storage { + Deque([visitor.visit(node: startNode, from: nil)]) + } + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - queue: The storage used by the strategy during traversal. + /// - edges: A closure that returns the edges for a given node. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from queue: inout Storage, edges: (Node) -> some Sequence>) -> Visit? { + guard let visit = queue.popFirst() else { return nil } + let visits = edges(node(from: visit)).map { edge in + visitor.visit(node: edge.destination, from: (visit, edge)) + } + queue.append(contentsOf: visits) + return visit + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + visitor.node(from: visit) + } +} diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Postorder.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Postorder.swift new file mode 100644 index 0000000..f87b7d3 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Postorder.swift @@ -0,0 +1,82 @@ +import Collections + +extension DFSOrder { + /// Creates a postorder depth-first search order. + /// - Returns: An instance of `DFSOrder` configured for postorder traversal. + @inlinable public static func postorder() -> Self where Self == DFSOrder> { + .init() + } +} + +extension GraphTraversalStrategy { + /// Creates a depth-first search strategy with the specified visitor and order. + /// - Parameters: + /// - visitor: The visitor to use during traversal. + /// - order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchPostorder` configured with the specified visitor and order. + @inlinable public static func dfs( + _ visitor: Visitor, + order: DFSOrder> + ) -> Self where Self == DepthFirstSearchPostorder { + .init(visitor: visitor) + } + + /// Creates a depth-first search strategy with the specified order. + /// - Parameter order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchPostorder` configured with the specified order. + @inlinable public static func dfs( + order: DFSOrder>> + ) -> Self where Self == DepthFirstSearchPostorder> { + .init(visitor: .onlyNodes()) + } +} + +/// A depth-first search strategy that performs a postorder traversal. +public struct DepthFirstSearchPostorder: GraphTraversalStrategy { + public typealias Storage = Deque<(isFirst: Bool, visit: Visitor.Visit)> + public typealias Node = Visitor.Node + public typealias Edge = Visitor.Edge + public typealias Visit = Visitor.Visit + + /// The visitor used during traversal. + @usableFromInline let visitor: Visitor + + /// Initializes a new postorder depth-first search strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + @inlinable public init(visitor: Visitor) { + self.visitor = visitor + } + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable public func initializeStorage(startNode: Node) -> Storage { + Deque([(isFirst: true, visit: visitor.visit(node: startNode, from: nil))]) + } + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - stack: The storage used by the strategy during traversal. + /// - edges: A closure that returns the edges for a given node. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from stack: inout Storage, edges: (Node) -> some Sequence>) -> Visit? { + guard let (isFirst, visit) = stack.popLast() else { return nil } + if isFirst { + stack.append((isFirst: false, visit: visit)) + let newVisits = edges(node(from: visit)).reversed().map { edge in + (isFirst: true, visit: visitor.visit(node: edge.destination, from: (visit, edge))) + } + stack.append(contentsOf: newVisits) + return next(from: &stack, edges: edges) + } else { + return visit + } + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + visitor.node(from: visit) + } +} diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Preorder.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Preorder.swift new file mode 100644 index 0000000..2b9dff5 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS+Preorder.swift @@ -0,0 +1,79 @@ +import Collections + +extension DFSOrder { + /// Creates a preorder depth-first search order. + /// - Returns: An instance of `DFSOrder` configured for preorder traversal. + @inlinable public static func preorder() -> Self where Self == DFSOrder> { + .init() + } +} + +extension GraphTraversalStrategy { + /// Creates a depth-first search strategy with the specified visitor and order. + /// - Parameters: + /// - visitor: The visitor to use during traversal. + /// - order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchPreorder` configured with the specified visitor and order. + @inlinable public static func dfs( + _ visitor: Visitor, + order: DFSOrder> + ) -> Self where Self == DepthFirstSearchPreorder { + .init(visitor: visitor) + } + + /// Creates a depth-first search strategy with the specified order. + /// - Parameter order: The order in which to perform the depth-first search. + /// - Returns: An instance of `DepthFirstSearchPreorder` configured with the specified order. + @inlinable public static func dfs( + order: DFSOrder>> + ) -> Self where Self == DepthFirstSearchPreorder> { + .init(visitor: .onlyNodes()) + } +} + +/// A depth-first search strategy that performs a preorder traversal. +public struct DepthFirstSearchPreorder: GraphTraversalStrategy { + public typealias Storage = Deque + public typealias Node = Visitor.Node + public typealias Edge = Visitor.Edge + public typealias Visit = Visitor.Visit + + /// The visitor used during traversal. + @usableFromInline let visitor: Visitor + + /// Initializes a new preorder depth-first search strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + @inlinable public init(visitor: Visitor) { + self.visitor = visitor + } + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable public func initializeStorage(startNode: Node) -> Storage { + Deque([visitor.visit(node: startNode, from: nil)]) + } + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - stack: The storage used by the strategy during traversal. + /// - edges: A closure that returns the edges for a given node. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from stack: inout Storage, edges: (Node) -> some Sequence>) -> Visit? { + guard let visit = stack.popLast() else { return nil } + let visits = edges(node(from: visit)).reversed().map { edge in + visitor.visit(node: edge.destination, from: (visit, edge)) + } + stack.append(contentsOf: visits) + return visit + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + visitor.node(from: visit) + } +} + + diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS.swift new file mode 100644 index 0000000..dc8363c --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy+DFS.swift @@ -0,0 +1,24 @@ +import Collections + +/// A structure representing the order of a depth-first search traversal. +public struct DFSOrder { + /// Initializes a new `DFSOrder` instance. + @inlinable public init() {} +} + +extension GraphTraversalStrategy { + /// Creates a depth-first search traversal strategy with the specified visitor. + /// - Parameter visitor: The visitor to use during traversal. + /// - Returns: A `DepthFirstSearchPreorder` instance configured with the specified visitor. + @inlinable public static func dfs( + _ visitor: Visitor + ) -> Self where Self == DepthFirstSearchPreorder { + .init(visitor: visitor) + } + + /// Creates a depth-first search traversal strategy with a default node visitor. + /// - Returns: A `DepthFirstSearchPreorder` instance configured with a default node visitor. + @inlinable public static func dfs() -> Self where Self == DepthFirstSearchPreorder> { + .init(visitor: .onlyNodes()) + } +} diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy+Unique.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy+Unique.swift new file mode 100644 index 0000000..8f92b30 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy+Unique.swift @@ -0,0 +1,73 @@ +extension GraphTraversalStrategy { + /// Creates a traversal strategy that visits each node once. + /// - Returns: A `UniqueTraversalStrategy` instance configured to visit each node once. + @inlinable public func visitEachNodeOnce() -> UniqueTraversalStrategy where Node: Hashable { + visitEachNodeOnce(by: \.self) + } + + /// Creates a traversal strategy that visits each node once, using a custom hash value. + /// - Parameter hashValue: A closure that takes a node and returns its hash value. + /// - Returns: A `UniqueTraversalStrategy` instance configured to visit each node once using the custom hash value. + @inlinable public func visitEachNodeOnce(by hashValue: @escaping (Node) -> HashValue) -> UniqueTraversalStrategy { + .init(base: self, hashValue: hashValue) + } +} + +/// A traversal strategy that ensures each node is visited only once. +public struct UniqueTraversalStrategy: GraphTraversalStrategy { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + public typealias Visit = Base.Visit + public typealias Storage = (base: Base.Storage, visited: Set) + + /// The base traversal strategy. + public var base: Base + /// A closure to compute the hash value of a node. + public var hashValue: (Node) -> HashValue + + /// Initializes a new unique traversal strategy with the given base strategy and hash value function. + /// - Parameters: + /// - base: The base traversal strategy. + /// - hashValue: A closure that takes a node and returns its hash value. + @inlinable public init(base: Base, hashValue: @escaping (Node) -> HashValue) { + self.base = base + self.hashValue = hashValue + } + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable public func initializeStorage(startNode: Node) -> Storage { + (base: base.initializeStorage(startNode: startNode), visited: []) + } + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - storage: The storage used by the strategy during traversal. + /// - edges: A closure that returns the edges for a given node. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from storage: inout Storage, edges: (Node) -> some Sequence>) -> Visit? { + while let visit = base.next(from: &storage.base, edges: { + edges($0).filter { + !storage.visited.contains(hashValue($0.destination)) + } + }) { + let node = base.node(from: visit) + if storage.visited.contains(hashValue(node)) { + continue + } + defer { + storage.visited.insert(hashValue(node)) + } + return visit + } + return nil + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + base.node(from: visit) + } +} diff --git a/Sources/Graph/Traversal/Graph+TraversalStrategy.swift b/Sources/Graph/Traversal/Graph+TraversalStrategy.swift new file mode 100644 index 0000000..e739e68 --- /dev/null +++ b/Sources/Graph/Traversal/Graph+TraversalStrategy.swift @@ -0,0 +1,39 @@ +/// A protocol that defines the requirements for a graph traversal strategy. +public protocol GraphTraversalStrategy { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. + associatedtype Edge + /// The type of visits produced during traversal. + associatedtype Visit + /// The type of storage used by the strategy during traversal. + associatedtype Storage + + /// Initializes the storage for the traversal starting from the specified node. + /// - Parameter startNode: The node from which to start the traversal. + /// - Returns: The initialized storage for the traversal. + @inlinable func initializeStorage(startNode: Node) -> Storage + + /// Advances to the next visit in the traversal sequence. + /// - Parameters: + /// - storage: The storage used by the strategy during traversal. + /// - edges: A closure that returns the edges for a given node. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable func next(from storage: inout Storage, edges: (Node) -> some Sequence>) -> Visit? + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable func node(from visit: Visit) -> Node +} + +extension GraphTraversalStrategy { + /// Advances to the next visit in the traversal sequence using a graph. + /// - Parameters: + /// - storage: The storage used by the strategy during traversal. + /// - graph: The graph being traversed. + /// - Returns: The next visit in the traversal sequence, or `nil` if there are no more visits. + @inlinable public func next(from storage: inout Storage, graph: some GraphProtocol) -> Visit? { + next(from: &storage, edges: graph.edges) + } +} diff --git a/Sources/Graph/Utils.swift b/Sources/Graph/Utils.swift new file mode 100644 index 0000000..7d5b8c3 --- /dev/null +++ b/Sources/Graph/Utils.swift @@ -0,0 +1,33 @@ +/// A protocol that defines a container for elements. +public protocol Container { + /// The type of elements contained in the container. + associatedtype Element + + /// An array of elements contained in the container. + var elements: [Element] { get } +} + +extension Array: Container { + /// An array of elements contained in the array. + @inlinable public var elements: [Element] { self } +} + +/// Returns the non-negative modulo of `lhs` by `rhs`. +/// - Parameters: +/// - lhs: The dividend. +/// - rhs: The divisor. +/// - Returns: The non-negative remainder of `lhs` divided by `rhs`. +@inlinable public func nonNegativeModulo(of lhs: Int, by rhs: Int) -> Int { + let result = lhs % rhs + return result >= 0 ? result : result + rhs +} + +extension Collection { + /// Returns the element at the specified position if it is within bounds, otherwise `nil`. + /// - Parameter position: The position of the element to retrieve. + /// - Returns: The element at the specified position if it is within bounds, otherwise `nil`. + @inlinable public subscript(safe position: Index) -> Element? where Index == Int { + let index = self.index(startIndex, offsetBy: position) + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/Graph/Visit/Graph+Visitor+Depth.swift b/Sources/Graph/Visit/Graph+Visitor+Depth.swift new file mode 100644 index 0000000..31cd0f0 --- /dev/null +++ b/Sources/Graph/Visit/Graph+Visitor+Depth.swift @@ -0,0 +1,49 @@ +extension VisitorProtocol { + /// Creates a visitor that tracks the depth of each node during traversal. + /// - Returns: An instance of `DepthVisitor` configured to track the depth of each node. + @inlinable public static func trackDepth() -> Self where Self == DepthTrackingVisitor> { + NodeVisitor().trackDepth() + } + + /// Wraps the current visitor to track the depth of each node during traversal. + /// - Returns: An instance of `DepthVisitor` that wraps the current visitor. + @inlinable public func trackDepth() -> DepthTrackingVisitor { + DepthTrackingVisitor(base: self) + } +} + +/// A visitor that tracks the depth of each node during traversal. +public struct DepthTrackingVisitor: VisitorProtocol { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + public typealias Visit = (base: Base.Visit, node: Node, depth: Int) + + /// The base visitor used during traversal. + @usableFromInline let base: Base + + /// Initializes a new `DepthVisitor` with the given base visitor. + /// - Parameter base: The base visitor to wrap. + @inlinable public init(base: Base) { + self.base = base + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + base.node(from: visit.base) + } + + /// Visits a node during traversal, tracking the depth of the node. + /// - Parameters: + /// - node: The node being visited. + /// - previousVisit: An optional tuple containing the previous visit and the edge leading to the current node. + /// - Returns: A visit instance representing the current visit, including the depth of the node. + @inlinable public func visit(node: Node, from previousVisit: (visit: Visit, edge: GraphEdge)?) -> Visit { + ( + base: base.visit(node: node, from: previousVisit.map { ($0.visit.base, $0.edge) }), + node: node, + depth: previousVisit.map { $0.visit.depth + 1 } ?? 0 + ) + } +} diff --git a/Sources/Graph/Visit/Graph+Visitor+Path.swift b/Sources/Graph/Visit/Graph+Visitor+Path.swift new file mode 100644 index 0000000..a860f98 --- /dev/null +++ b/Sources/Graph/Visit/Graph+Visitor+Path.swift @@ -0,0 +1,81 @@ +extension VisitorProtocol { + /// Creates a visitor that tracks the path of each node during traversal. + /// - Returns: An instance of `PathVisitor` configured to track the path of each node. + @inlinable public static func trackPath() -> Self where Self == PathTrackingVisitor> { + NodeVisitor().trackPath() + } + + /// Wraps the current visitor to track the path of each node during traversal. + /// - Returns: An instance of `PathVisitor` that wraps the current visitor. + @inlinable public func trackPath() -> PathTrackingVisitor { + PathTrackingVisitor(base: self) + } +} + +/// A visitor that tracks the path of each node during traversal. +public struct PathTrackingVisitor: VisitorProtocol { + public typealias Node = Base.Node + public typealias Edge = Base.Edge + + /// A structure representing a visit during traversal, including the path of edges. + public struct Visit { + /// The base visit. + public let base: Base.Visit + /// The node being visited. + public let node: Base.Node + /// The edges leading to the node. + public let edges: [GraphEdge] + + /// Initializes a new `Visit` instance with the given base visit, node, and edges. + /// - Parameters: + /// - base: The base visit. + /// - node: The node being visited. + /// - edges: The edges leading to the node. + @inlinable public init(base: Base.Visit, node: Node, edges: [GraphEdge]) { + self.base = base + self.node = node + self.edges = edges + } + + /// The path of nodes from the start node to the current node. + @inlinable public var path: [Node] { + edges.map(\.source) + [node] + } + } + + /// The base visitor used during traversal. + @usableFromInline let base: Base + + /// Initializes a new `PathTrackingVisitor` with the given base visitor. + /// - Parameter base: The base visitor to wrap. + @inlinable public init(base: Base) { + self.base = base + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + base.node(from: visit.base) + } + + /// Visits a node during traversal, tracking the path of the connecting edges. + /// - Parameters: + /// - node: The node being visited. + /// - previousVisit: An optional tuple containing the previous visit and the edge leading to the current node. + /// - Returns: A visit instance representing the current visit, including the path of the connecting edges. + @inlinable public func visit(node: Node, from previousVisit: (visit: Visit, edge: GraphEdge)?) -> Visit { + Visit( + base: base.visit(node: node, from: previousVisit.map { ($0.visit.base, $0.edge) }), + node: node, + edges: previousVisit.map { $0.visit.edges + [$0.edge] } ?? [] + ) + } +} + +extension PathTrackingVisitor.Visit where Base.Edge: Weighted, Base.Edge.Weight: Numeric { + /// The cost, representing the sum of the weights along the path of edges + @inlinable public var cost: Base.Edge.Weight { + edges.lazy.map(\.value.weight).reduce(into: .zero, +=) + } +} diff --git a/Sources/Graph/Visit/Graph+Visitor.swift b/Sources/Graph/Visit/Graph+Visitor.swift new file mode 100644 index 0000000..6bdcb18 --- /dev/null +++ b/Sources/Graph/Visit/Graph+Visitor.swift @@ -0,0 +1,54 @@ +/// A protocol that defines the requirements for a visitor in a graph traversal. +public protocol VisitorProtocol { + /// The type of nodes in the graph. + associatedtype Node + /// The type of edges in the graph. + associatedtype Edge + /// The type of visits produced during traversal. + associatedtype Visit + + /// Visits a node during traversal. + /// - Parameters: + /// - node: The node being visited. + /// - previousVisit: An optional tuple containing the previous visit and the edge leading to the current node. + /// - Returns: A visit instance representing the current visit. + @inlinable func visit(node: Node, from previousVisit: (visit: Visit, edge: GraphEdge)?) -> Visit + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable func node(from visit: Visit) -> Node +} + +extension VisitorProtocol { + /// Creates a visitor that only visits nodes. + /// - Returns: An instance of `NodeVisitor` configured to only visit nodes. + @inlinable public static func onlyNodes() -> Self where Self == NodeVisitor { + NodeVisitor() + } +} + +/// A visitor that only visits nodes in a graph traversal. +public struct NodeVisitor: VisitorProtocol { + public typealias Visit = Node + public typealias Storage = Void + + /// Initializes a new `NodeVisitor` instance. + @inlinable public init() {} + + /// Visits a node during traversal. + /// - Parameters: + /// - node: The node being visited. + /// - previousVisit: An optional tuple containing the previous visit and the edge leading to the current node. + /// - Returns: The node being visited. + @inlinable public func visit(node: Node, from: (visit: Visit, edge: GraphEdge)?) -> Visit { + node + } + + /// Extracts the node from a visit. + /// - Parameter visit: The visit from which to extract the node. + /// - Returns: The node associated with the visit. + @inlinable public func node(from visit: Visit) -> Node { + visit + } +} diff --git a/Sources/Graph/Weighted.swift b/Sources/Graph/Weighted.swift new file mode 100644 index 0000000..e1274a5 --- /dev/null +++ b/Sources/Graph/Weighted.swift @@ -0,0 +1,30 @@ +/// A protocol that defines a weighted element. +public protocol Weighted { + /// The type of the weight, which must be comparable. + associatedtype Weight: Comparable + + /// The weight of the element. + var weight: Weight { get } +} + +extension Weighted where Self: Comparable { + /// Returns the weight of the element. + @inlinable public var weight: Self { self } +} + +extension Comparable where Self: Weighted { + /// Compares two weighted elements based on their weights. + /// - Parameters: + /// - lhs: The left-hand side element. + /// - rhs: The right-hand side element. + /// - Returns: `true` if the weight of the left-hand side element is less than the weight of the right-hand side element, `false` otherwise. + @inlinable public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.weight < rhs.weight + } +} + +extension Int: Weighted {} +extension UInt: Weighted {} +extension Double: Weighted {} +extension Float: Weighted {} + diff --git a/Sources/Graph/WholeGraphProtocol+Properties.swift b/Sources/Graph/WholeGraphProtocol+Properties.swift new file mode 100644 index 0000000..260e613 --- /dev/null +++ b/Sources/Graph/WholeGraphProtocol+Properties.swift @@ -0,0 +1,191 @@ +extension WholeGraphProtocol where Node: Hashable { + /// Checks if the graph is connected. + /// - Returns: `true` if the graph is connected, `false` otherwise. + @inlinable public func isConnected() -> Bool { + if allNodes.isEmpty { + return true + } + for node in allNodes where traverse(from: node, strategy: .bfs().visitEachNodeOnce()).count == allNodes.count { + return true + } + return false + } + + /// Checks if the graph contains a cycle. + /// - Returns: `true` if the graph contains a cycle, `false` otherwise. + @inlinable public func isCyclic() -> Bool { + var visited = Set() + for node in allNodes where !visited.contains(node) { + for newNode in traversal(from: node, strategy: .dfs()) { + visited.insert(newNode) + } + } + return false + } + + /// Checks if the graph is a tree. + /// - Returns: `true` if the graph is a tree, `false` otherwise. + @inlinable public func isTree() -> Bool { + return isConnected() && !isCyclic() + } + + /// Checks if the graph has an Eulerian path. + /// - Returns: `true` if the graph has an Eulerian path, `false` otherwise. + @inlinable public func hasEulerianPath() -> Bool { + eulerianProperties().hasEulerianPath + } + + /// Checks if the graph has an Eulerian cycle. + /// - Returns: `true` if the graph has an Eulerian cycle, `false` otherwise. + @inlinable public func hasEulerianCycle() -> Bool { + eulerianProperties().hasEulerianCycle + } + + /// Computes the Eulerian properties of the graph. + /// - Returns: A tuple containing two Boolean values indicating whether the graph has an Eulerian path and an Eulerian cycle. + @inlinable public func eulerianProperties() -> (hasEulerianPath: Bool, hasEulerianCycle: Bool) { + guard isConnected() else { + return (false, false) + } + + var inDegree: [Node: Int] = [:] + var outDegree: [Node: Int] = [:] + + for edge in allEdges { + outDegree[edge.source, default: 0] += 1 + inDegree[edge.destination, default: 0] += 1 + } + + var startNodeCount = 0 + var endNodeCount = 0 + var isBalanced = true + + for node in allNodes { + let out = outDegree[node] ?? 0 + let `in` = inDegree[node] ?? 0 + + if out == `in` + 1 { + startNodeCount += 1 + if startNodeCount > 1 { + isBalanced = false + break + } + } else if `in` == out + 1 { + endNodeCount += 1 + if endNodeCount > 1 { + isBalanced = false + break + } + } else if `in` != out { + isBalanced = false + break + } + } + + let hasEulerianCycle = isBalanced && startNodeCount == 0 && endNodeCount == 0 + let hasEulerianPath = (startNodeCount == 1 && endNodeCount == 1) || hasEulerianCycle + return (hasEulerianPath, hasEulerianCycle) + } + + /// Returns the out-degree of the specified node. + /// - Parameter node: The node for which to calculate the out-degree. + /// - Returns: The out-degree of the specified node. + @inlinable public func outDegree(of node: Node) -> Int { + allEdges.filter { $0.source == node }.count + } + + /// Returns the in-degree of the specified node. + /// - Parameter node: The node for which to calculate the in-degree. + /// - Returns: The in-degree of the specified node. + @inlinable public func inDegree(of node: Node) -> Int { + allEdges.filter { $0.destination == node }.count + } + + /// Checks if two nodes are adjacent. + /// - Parameters: + /// - node1: The first node. + /// - node2: The second node. + /// - Returns: `true` if the nodes are adjacent, `false` otherwise. + @inlinable public func isAdjacent(_ node1: Node, _ node2: Node) -> Bool { + allEdges.contains { ($0.source == node1 && $0.destination == node2) || ($0.source == node2 && $0.destination == node1) } + } + + /// Checks if the graph satisfies Dirac's theorem. + /// - Returns: `true` if the graph satisfies Dirac's theorem, `false` otherwise. + @inlinable public func satisfiesDirac() -> Bool { + let n = allNodes.count + guard n >= 3 else { return false } + return allNodes.allSatisfy { node in + outDegree(of: node) >= n / 2 + } + } + + /// Checks if the graph satisfies Ore's theorem. + /// - Returns: `true` if the graph satisfies Ore's theorem, `false` otherwise. + @inlinable public func satisfiesOre() -> Bool { + let n = allNodes.count + guard n >= 3 else { return false } + for node in allNodes { + for other in allNodes where other != node && !isAdjacent(node, other) { + if outDegree(of: node) + outDegree(of: other) < n { + return false + } + } + } + return true + } + + /// Performs a topological sort on the graph. + /// - Returns: An array of nodes in topologically sorted order, or `nil` if the graph contains a cycle. + @inlinable public func topologicalSort() -> [Node]? { + let count = allNodes.count + return allNodes.lazy.map { + self.traverse(from: $0, strategy: .dfs().visitEachNodeOnce()) + }.first { $0.count == count } + } + + /// Checks if the graph is planar. + /// - Returns: `true` if the graph is planar, `false` otherwise. + @inlinable public func isPlanar() -> Bool { + let v = allNodes.count + let e = allEdges.count + + if v <= 4 { + return true + } + + return e <= 3 * v - 6 + } + + /// Checks if the graph is bipartite. + /// - Returns: `true` if the graph is bipartite, `false` otherwise. + @inlinable public func isBipartite() -> Bool { + func bipartiteDFS(node: Node, color: inout [Node: Int], currentColor: Int) -> Bool { + color[node] = currentColor + + for edge in edges(from: node) { + let neighbor = edge.destination + + if color[neighbor] == nil { + if !bipartiteDFS(node: neighbor, color: &color, currentColor: 1 - currentColor) { + return false + } + } else if color[neighbor] == color[node] { + return false + } + } + + return true + } + + var color: [Node: Int] = [:] + for node in allNodes { + if color[node] == nil { + if !bipartiteDFS(node: node, color: &color, currentColor: 0) { + return false + } + } + } + return true + } +} diff --git a/Sources/Graph/WholeGraphProtocol.swift b/Sources/Graph/WholeGraphProtocol.swift new file mode 100644 index 0000000..24b8a9d --- /dev/null +++ b/Sources/Graph/WholeGraphProtocol.swift @@ -0,0 +1,23 @@ +/// A protocol that extends `GraphProtocol` to include methods for accessing all nodes and edges in the graph. +public protocol WholeGraphProtocol: GraphProtocol { + /// An array containing all nodes in the graph. + var allNodes: [Node] { get } + /// An array containing all edges in the graph. + var allEdges: [GraphEdge] { get } +} + +extension WholeGraphProtocol { + /// A default implementation that returns all edges in the graph by flattening the edges from all nodes. + @inlinable public var allEdges: [GraphEdge] { + allNodes.flatMap(edges) + } +} + +extension GraphProtocol where Self: WholeGraphProtocol, Node: Equatable { + /// Returns the edges originating from the specified node. + /// - Parameter node: The node from which to get the edges. + /// - Returns: An array of `GraphEdge` instances containing the edges from the specified node. + @inlinable public func edges(from node: Node) -> [GraphEdge] { + allEdges.filter { $0.source == node } + } +} diff --git a/Tests/GraphTests/GraphTests.swift b/Tests/GraphTests/GraphTests.swift new file mode 100644 index 0000000..5b36011 --- /dev/null +++ b/Tests/GraphTests/GraphTests.swift @@ -0,0 +1,248 @@ +import Graphs +import Testing + +struct GraphStrategy { + @Test func graph() { + let graph = Graph(edges: [ + "Root": ["A", "B", "C"], + "B": ["X", "Y", "Z"], + "Y": ["N", "M"] + ]) + + let bfs = ["Root", "A", "B", "C", "X", "Y", "Z", "N", "M"] + #expect(graph.traverse(from: "Root", strategy: .bfs()) == bfs) + #expect(graph.traverse(from: "Root", strategy: .bfs(.onlyNodes())) == bfs) + + let preorder = ["Root", "A", "B", "X", "Y", "N", "M", "Z", "C"] + #expect(graph.traverse(from: "Root", strategy: .dfs()) == preorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes())) == preorder) + + #expect(graph.traverse(from: "Root", strategy: .dfs(order: .preorder())) == preorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes(), order: .preorder())) == preorder) + + let postorder = ["A", "X", "N", "M", "Y", "Z", "B", "C", "Root"] + #expect(graph.traverse(from: "Root", strategy: .dfs(order: .postorder())) == postorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes(), order: .postorder())) == postorder) + } + + @Test func binaryGraph() { + let graph = BinaryGraph(edges: [ + "Root": (lhs: "A", rhs: "B"), + "A": (lhs: "X", rhs: "Y"), + "Y": (lhs: nil, rhs: "N") + ]) + + let bfs = ["Root", "A", "B", "X", "Y", "N"] + #expect(graph.traverse(from: "Root", strategy: .bfs()) == bfs) + #expect(graph.traverse(from: "Root", strategy: .bfs(.onlyNodes())) == bfs) + + let preorder = ["Root", "A", "X", "Y", "N", "B"] + #expect(graph.traverse(from: "Root", strategy: .dfs()) == preorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes())) == preorder) + + #expect(graph.traverse(from: "Root", strategy: .dfs(order: .preorder())) == preorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes(), order: .preorder())) == preorder) + + let inorder = ["X", "A", "Y", "N", "Root", "B"] + #expect(graph.traverse(from: "Root", strategy: .dfs(order: .inorder())) == inorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.onlyNodes(), order: .inorder())) == inorder) + + let postorder = ["X", "N", "Y", "A", "B", "Root"] + #expect(graph.traverse(from: "Root", strategy: .dfs(order: .postorder())) == postorder) + #expect(graph.traverse(from: "Root", strategy: .dfs(.trackDepth(), order: .postorder())).compactMap(\.node) == postorder) + } + + @Test func unique() { + let graph = Graph(edges: [ + "Root": ["A", "B", "C"], + "B": ["X", "Y", "A"] + ]) + + #expect(graph.traverse(from: "Root", strategy: .bfs()) == ["Root", "A", "B", "C", "X", "Y", "A"]) + #expect(graph.traverse(from: "Root", strategy: .bfs().visitEachNodeOnce()) == ["Root", "A", "B", "C", "X", "Y"]) + } + + @Test func trace() { + let graph = Graph(edges: [ + "Root": ["A", "B", "C"], + "B": ["X", "Y", "A"] + ]) + + #expect(graph.traverse(from: "Root", strategy: .bfs(.trackDepth())).map(\.node) == ["Root", "A", "B", "C", "X", "Y", "A"]) + #expect(graph.traversal(from: "Root", strategy: .bfs(.trackDepth())).map(\.depth) == [0, 1, 1, 1, 2, 2, 2]) + #expect(graph.traversal(from: "Root", strategy: .bfs(.trackPath())).map(\.path) == [ + ["Root"], + ["Root", "A"], + ["Root", "B"], + ["Root", "C"], + ["Root", "B", "X"], + ["Root", "B", "Y"], + ["Root", "B", "A"] + ]) + } + + @Test func shortestPath() { + let graph = Graph(edges: [ + "Root": ["A": 2 as UInt, "B": 2, "C": 2], + "B": ["X": 2, "Y": 2, "Z": 20, "A": 2], + "Y": ["N": 2, "M": 2, "Z": 2] + ]) + + #expect(graph.shortestPath(from: "Root", to: "Z", using: .dijkstra())?.path == ["Root", "B", "Y", "Z"]) + #expect(graph.shortestPath(from: "Root", to: "Z", using: .dijkstra())?.cost == 6) + } + + @Test func shortestPathGrid() { + let graph = GridGraph(grid: [ + ["A", "B", "C", "D", "E"], + ["F", "G", "H", "I", "J"], + ["K", "L", "M", "N", "O"], + ["P", "Q", "R", "S", "T"], + ["U", "V", "W", "X", "Y"] + ], availableDirections: .orthogonal).weightedByDistance() + + #expect( + graph.shortestPath( + from: GridPosition(x: 0, y: 0), + to: GridPosition(x: 4, y: 4), + using: .dijkstra() + )?.path.map { graph[$0] } == ["A", "B", "G", "L", "Q", "V", "W", "X", "Y"] + ) + + #expect( + graph.shortestPath( + from: GridPosition(x: 0, y: 0), + to: GridPosition(x: 4, y: 4), + using: .aStar(heuristic: .euclideanDistance(of: \.coordinates)) + )?.path.map { graph[$0] } == ["A", "B", "G", "L", "Q", "V", "W", "X", "Y"] + ) + + #expect( + graph.shortestPath( + from: GridPosition(x: 0, y: 0), + to: GridPosition(x: 4, y: 4), + using: .aStar(heuristic: .manhattanDistance(of: \.coordinates)) + )?.path.map { graph[$0] } == ["A", "B", "G", "L", "Q", "V", "W", "X", "Y"] + ) + } + + @Test func shortestPaths() { + let graph = GridGraph(grid: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]).weightedByDistance() + #expect( + Set(graph.shortestPaths(from: GridPosition(x: 0, y: 0)).map { $0.value.path.map { graph[$0] } }) == [ + [1], + [1, 2], + [1, 2, 3], + [1, 4], + [1, 5], + [1, 2, 6], + [1, 5, 7], + [1, 5, 8], + [1, 5, 9] + ] + ) + } + + @Test func minimumSpanningTree() { + let graph = Graph(edges: [ + "A": ["B": 1, "C": 3], + "B": ["A": 1, "C": 1, "D": 4], + "C": ["A": 3, "B": 1, "D": 1], + "D": ["B": 4, "C": 1] + ]) + + #expect(graph.minimumSpanningTree(using: .kruskal()).sortedDescription() == ["A-B", "B-C", "C-D"]) + #expect(graph.minimumSpanningTree(using: .prim()).sortedDescription() == ["A-B", "B-C", "C-D"]) + } + + @Test func maxFlowMinCut() { + let graph = Graph(edges: [ + "S": ["A": 10, "B": 5], + "A": ["B": 15, "C": 10], + "B": ["D": 10], + "C": ["T": 10], + "D": ["C": 10, "T": 10] + ]) + +// #expect(graph.maximumFlow(from: "S", to: "T", using: .fordFulkerson()) == 15) +// #expect(graph.minimumCut(from: "S", to: "T", using: .fordFulkerson()).cutValue == 15) +// #expect(graph.minimumCut(from: "S", to: "T", using: .fordFulkerson()).cutEdges.sortedDescription() == ["A-S", "B-S"]) + + #expect(graph.maximumFlow(from: "S", to: "T", using: .edmondsKarp()) == 15) + #expect(graph.minimumCut(from: "S", to: "T", using: .edmondsKarp()).cutValue == 15) + #expect(graph.minimumCut(from: "S", to: "T", using: .edmondsKarp()).cutEdges.sortedDescription() == ["A-S", "B-S"]) + } + + @Test func hamiltonianPath() { + let small = GridGraph(grid: [ + ["A", "B"], + ["C", "D"] + ], availableDirections: .orthogonal) + #expect(small.hamiltonianPath(from: GridPosition(x: 0, y: 0))?.path.map { small[$0] } == ["A", "B", "D", "C"]) + #expect(small.hamiltonianCycle(from: GridPosition(x: 0, y: 0))?.path.map { small[$0] } == ["A", "B", "D", "C", "A"]) + + #expect(small.hamiltonianPath(from: GridPosition(x: 0, y: 0), using: .heuristic(.degree()))?.path.map { small[$0] } == ["A", "B", "D", "C"]) + #expect(small.hamiltonianCycle(from: GridPosition(x: 0, y: 0), using: .heuristic(.degree()))?.path.map { small[$0] } == ["A", "B", "D", "C", "A"]) + + let large = GridGraph(grid: [ + ["A", "B", "C", "D"], + ["E", "F", "G", "H"], + ["I", "J", "K", "L"], + ["M", "N", "O", "P"] + ], availableDirections: .orthogonal) + + #expect(large.hamiltonianPath()?.path.map { large[$0] } == ["A", "B", "C", "D", "H", "L", "P", "O", "N", "M", "I", "J", "K", "G", "F", "E"]) + #expect(large.hamiltonianPath(from: GridPosition(x: 0, y: 0))?.path.map { large[$0] } == ["A", "B", "C", "D", "H", "L", "P", "O", "N", "M", "I", "J", "K", "G", "F", "E"]) + #expect(large.hamiltonianPath(from: GridPosition(x: 0, y: 0), to: GridPosition(x: 3, y: 0))?.path.map { large[$0] } == ["A", "B", "C", "D", "H", "L", "P", "O", "N", "M", "I", "J", "K", "G", "F", "E"]) + + #expect(large.hamiltonianCycle()?.path.map { large[$0] } == ["A", "B", "C", "D", "H", "L", "P", "O", "N", "M", "I", "J", "K", "G", "F", "E", "A"]) + #expect(large.hamiltonianCycle(from: GridPosition(x: 1, y: 1))?.path.map { large[$0] } == ["F", "G", "H", "D", "C", "B", "A", "E", "I", "M", "N", "O", "P", "L", "K", "J", "F"]) + + #expect(large.hamiltonianPath(using: .heuristic(.degree()))?.path.map { large[$0] } == ["A", "B", "F", "E", "I", "M", "N", "J", "K", "O", "P", "L", "H", "G", "C", "D"]) + #expect(large.hamiltonianPath(from: GridPosition(x: 0, y: 0), using: .heuristic(.degree()))?.path.map { large[$0] } == ["A", "B", "F", "E", "I", "M", "N", "J", "K", "O", "P", "L", "H", "G", "C", "D"]) + #expect(large.hamiltonianPath(from: GridPosition(x: 0, y: 0), to: GridPosition(x: 3, y: 0), using: .heuristic(.degree()))?.path.map { large[$0] } == ["A", "B", "F", "E", "I", "M", "N", "J", "K", "O", "P", "L", "H", "G", "C", "D"]) + + #expect(large.hamiltonianCycle(using: .heuristic(.degree()))?.path.map { large[$0] } == ["A", "B", "F", "G", "C", "D", "H", "L", "P", "O", "K", "J", "N", "M", "I", "E", "A"]) + #expect(large.hamiltonianCycle(from: GridPosition(x: 1, y: 1), using: .heuristic(.degree()))?.path.map { large[$0] } == ["F", "J", "I", "M", "N", "O", "P", "L", "K", "G", "H", "D", "C", "B", "A", "E", "F"]) + } + + @Test func eulerianPath() { + let triangle = Graph(edges: [ + "A": ["B", "C"], + "B": ["A", "C"], + "C": ["A", "B"] + ]).weighted(constant: 1) + + #expect(triangle.eulerianCycle(from: "A")?.path == ["A", "B", "A", "C", "B", "C", "A"]) + #expect(triangle.eulerianCycle(from: "A", using: .hierholzer())?.path == ["A", "C", "B", "C", "A", "B", "A"]) + + let graph = Graph(edges: [ + "S": ["A"], + "A": ["B"], + "B": ["C"], + "C": ["D"] + ]).weighted(constant: 1) + + #expect(graph.eulerianPath()?.path == ["S", "A", "B", "C", "D"]) + #expect(graph.eulerianPath(from: "S")?.path == ["S", "A", "B", "C", "D"]) + #expect(graph.eulerianPath(from: "S", to: "D")?.path == ["S", "A", "B", "C", "D"]) + } + + @Test func bipartite() { + let graph = Graph(edges: [ + GraphEdge(source: "u1", destination: "v1"), + GraphEdge(source: "u1", destination: "v2"), + GraphEdge(source: "u2", destination: "v1"), + GraphEdge(source: "u3", destination: "v2"), + GraphEdge(source: "u3", destination: "v3"), + ]).bipartite(leftPartition: ["u1", "u2", "u3"], rightPartition: ["v1", "v2", "v3"]) + + #expect(graph.maximumMatching(using: .hopcroftKarp()) == ["u1": "v2", "u2": "v1", "u3": "v3"]) + } +} + +extension Sequence { + func sortedDescription() -> [String] where Element == GraphEdge { + map { [$0.source.description, $0.destination.description].sorted().joined(separator: "-") }.sorted() + } +}