Skip to content

Commit

Permalink
Graph library
Browse files Browse the repository at this point in the history
  • Loading branch information
tevelee committed Sep 29, 2024
0 parents commit ff4ac78
Show file tree
Hide file tree
Showing 77 changed files with 5,777 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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)]
)
]
)
137 changes: 137 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 49 additions & 0 deletions Sources/Graph/BinaryGraphProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// A protocol that defines the requirements for a binary graph structure.
public protocol BinaryGraphProtocol<Node, Edge>: 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<Node, Edge>
}

/// A structure representing the edges of a node in a binary graph.
public struct BinaryGraphEdges<Node, Edge>: Container {
/// The type of elements contained in the container.
public typealias Element = GraphEdge<Node, Edge>

/// 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<Node, Edge>] {
let edges: BinaryGraphEdges<Node, Edge> = self.edges(from: node)
return edges.elements
}
}
7 changes: 7 additions & 0 deletions Sources/Graph/BipartiteGraphProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// A protocol for bipartite graphs.
public protocol BipartiteGraphProtocol<Node, Edge>: GraphProtocol where Node: Hashable {
/// The left partition of the bipartite graph.
var leftPartition: Set<Node> { get }
/// The right partition of the bipartite graph.
var rightPartition: Set<Node> { get }
}
42 changes: 42 additions & 0 deletions Sources/Graph/Coloring/Graph+Coloring+Greedy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
extension GraphColoringAlgorithm {
/// Creates a new instance of the greedy algorithm.
@inlinable public static func greedy<Node, Edge>() -> Self where Self == GreedyColoringAlgorithm<Node, Edge> {
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<Node: Hashable, Edge>: 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, Edge>
) -> [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<Int> = []
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
}
}
25 changes: 25 additions & 0 deletions Sources/Graph/Coloring/Graph+Coloring.swift
Original file line number Diff line number Diff line change
@@ -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, Edge>
) -> [Node: Int] {
algorithm.coloring(of: self)
}
}

/// A protocol that defines the requirements for a graph coloring algorithm.
public protocol GraphColoringAlgorithm<Node, Edge> {
/// 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, Edge>
) -> [Node: Int]
}
Loading

0 comments on commit ff4ac78

Please sign in to comment.