Skip to content

Add UndirectedGraph and DirectedGraph data structures #7460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/Basics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ add_library(Basics
FileSystem/TemporaryFile.swift
FileSystem/TSCAdapters.swift
FileSystem/VFSOverlay.swift
Graph/AdjacencyMatrix.swift
Graph/DirectedGraph.swift
Graph/UndirectedGraph.swift
SourceControlURL.swift
HTTPClient/HTTPClient.swift
HTTPClient/HTTPClientConfiguration.swift
Expand Down
61 changes: 61 additions & 0 deletions Sources/Basics/Graph/AdjacencyMatrix.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// A matrix storing bits of `true`/`false` state for a given combination of row and column indices. Used as
/// a square matrix indicating edges in graphs, where rows and columns are indices in a storage of graph's nodes.
///
/// For example, in a graph that contains 3 nodes `matrix[row: 1, column: 2]` evaluating to `true` means an edge
/// between nodes with indices `1` and `2` exists. `matrix[row: 1, column: 2]` evaluating to `false` means that no
/// edge exists.
///
/// See https://en.wikipedia.org/wiki/Adjacency_matrix for more details.
struct AdjacencyMatrix {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the difference in adjacency list vs matrix between the directed and undirected graphs?

Copy link
Contributor Author

@MaxDesiatov MaxDesiatov Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with an adjacency list in a directed graph implementation, but then proceeded to an undirected graph where adjacency matrix seemed obviously more performant when adding edges: just flipping two bits in the matrix instead of adding potential array (re-)allocations with the list.

I'll add benchmarks to get a confirmation for that. I'm unsure though if either is a clear win in all cases, adjacency matrix may be fast at adding edges, but not the fastest for different kinds of graphs analysis and lookups. Keeping both implementations for now for future reference seemed like a sensible idea given that it's an NFC PR anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure though if either is a clear win in all cases

Yeah, really depends on what type of graphs we have and what the main operation on them is. If they're sparse (which I would assume is the case) and we mainly want to loop over edges (rather than check if there is an edge), lists are likely better. Happy for you to keep both for now if you want.

let columns: Int
let rows: Int
private var bytes: [UInt8]

/// Allocates a new bit matrix with a given size.
/// - Parameters:
/// - rows: Number of rows in the matrix.
/// - columns: Number of columns in the matrix.
init(rows: Int, columns: Int) {
self.columns = columns
self.rows = rows

let (quotient, remainder) = (rows * columns).quotientAndRemainder(dividingBy: 8)
self.bytes = .init(repeating: 0, count: quotient + (remainder > 0 ? 1 : 0))
}

var bitCount: Int {
bytes.count * 8
}

private func calculateOffsets(row: Int, column: Int) -> (byteOffset: Int, bitOffsetInByte: Int) {
let totalBitOffset = row * columns + column
return (byteOffset: totalBitOffset / 8, bitOffsetInByte: totalBitOffset % 8)
}

subscript(row: Int, column: Int) -> Bool {
get {
let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column)

let result = (self.bytes[byteOffset] >> bitOffsetInByte) & 1
return result == 1
}

set {
let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column)

self.bytes[byteOffset] |= 1 << bitOffsetInByte
}
}
}
54 changes: 54 additions & 0 deletions Sources/Basics/Graph/DirectedGraph.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import struct DequeModule.Deque

/// Directed graph that stores edges in [adjacency lists](https://en.wikipedia.org/wiki/Adjacency_list).
struct DirectedGraph<Node> {
init(nodes: [Node]) {
self.nodes = nodes
self.edges = .init(repeating: [], count: nodes.count)
}

private var nodes: [Node]
private var edges: [[Int]]

mutating func addEdge(source: Int, destination: Int) {
self.edges[source].append(destination)
}

/// Checks whether a path via previously created edges between two given nodes exists.
/// - Parameters:
/// - source: `Index` of a node to start traversing edges from.
/// - destination: `Index` of a node to which a path could exist via edges from `source`.
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
func areNodesConnected(source: Int, destination: Int) -> Bool {
var todo = Deque<Int>([source])
var done = Set<Int>()

while !todo.isEmpty {
let nodeIndex = todo.removeFirst()

for reachableIndex in self.edges[nodeIndex] {
if reachableIndex == destination {
return true
} else if !done.contains(reachableIndex) {
todo.append(reachableIndex)
}
}

done.insert(nodeIndex)
}

return false
}
}
68 changes: 68 additions & 0 deletions Sources/Basics/Graph/UndirectedGraph.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import struct DequeModule.Deque

/// Undirected graph that stores edges in an [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_list).
struct UndirectedGraph<Node> {
init(nodes: [Node]) {
self.nodes = nodes
self.edges = .init(rows: nodes.count, columns: nodes.count)
}

private var nodes: [Node]
private var edges: AdjacencyMatrix

mutating func addEdge(source: Int, destination: Int) {
// Adjacency matrix is symmetrical for undirected graphs.
self.edges[source, destination] = true
self.edges[destination, source] = true
}

/// Checks whether a connection via previously created edges between two given nodes exists.
/// - Parameters:
/// - source: `Index` of a node to start traversing edges from.
/// - destination: `Index` of a node to which a connection could exist via edges from `source`.
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
func areNodesConnected(source: Int, destination: Int) -> Bool {
var todo = Deque<Int>([source])
var done = Set<Int>()

while !todo.isEmpty {
let nodeIndex = todo.removeFirst()

for reachableIndex in self.edges.nodesAdjacentTo(nodeIndex) {
if reachableIndex == destination {
return true
} else if !done.contains(reachableIndex) {
todo.append(reachableIndex)
}
}

done.insert(nodeIndex)
}

return false
}
}

private extension AdjacencyMatrix {
func nodesAdjacentTo(_ nodeIndex: Int) -> [Int] {
var result = [Int]()

for i in 0..<self.rows where self[i, nodeIndex] {
result.append(i)
}

return result
}
}
40 changes: 40 additions & 0 deletions Tests/BasicsTests/Graph/AdjacencyMatrixTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@testable import Basics
import XCTest

final class AdjacencyMatrixTests: XCTestCase {
func testEmpty() {
var matrix = AdjacencyMatrix(rows: 0, columns: 0)
XCTAssertEqual(matrix.bitCount, 0)

matrix = AdjacencyMatrix(rows: 0, columns: 42)
XCTAssertEqual(matrix.bitCount, 0)

matrix = AdjacencyMatrix(rows: 42, columns: 0)
XCTAssertEqual(matrix.bitCount, 0)
}

func testBits() {
for count in 1..<10 {
var matrix = AdjacencyMatrix(rows: count, columns: count)
for row in 0..<count {
for column in 0..<count {
XCTAssertFalse(matrix[row, column])
matrix[row, column] = true
XCTAssertTrue(matrix[row, column])
}
}
}
}
}
31 changes: 31 additions & 0 deletions Tests/BasicsTests/Graph/DirectedGraphTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@testable import Basics
import XCTest

final class DirectedGraphTests: XCTestCase {
func testNodesConnection() {
var graph = DirectedGraph(nodes: ["app1", "lib1", "lib2", "app2", "lib3"])
graph.addEdge(source: 0, destination: 1)
graph.addEdge(source: 1, destination: 2)
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 2))
XCTAssertFalse(graph.areNodesConnected(source: 2, destination: 0))

graph.addEdge(source: 0, destination: 4)
graph.addEdge(source: 3, destination: 4)
XCTAssertTrue(graph.areNodesConnected(source: 3, destination: 4))
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
XCTAssertFalse(graph.areNodesConnected(source: 1, destination: 4))
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
}
}
38 changes: 38 additions & 0 deletions Tests/BasicsTests/Graph/UndirectedGraphTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@testable import Basics
import XCTest

final class UndirectedGraphTests: XCTestCase {
func testNodesConnection() {
var graph = UndirectedGraph(nodes: ["app1", "lib1", "lib2", "app2", "lib3", "app3"])
graph.addEdge(source: 0, destination: 1)
graph.addEdge(source: 1, destination: 2)
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 2))
XCTAssertTrue(graph.areNodesConnected(source: 2, destination: 0))

graph.addEdge(source: 0, destination: 4)
graph.addEdge(source: 3, destination: 4)
XCTAssertTrue(graph.areNodesConnected(source: 3, destination: 4))
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 3))
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 0))
XCTAssertTrue(graph.areNodesConnected(source: 1, destination: 4))
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 1))

for i in 0...4 {
XCTAssertFalse(graph.areNodesConnected(source: i, destination: 5))
XCTAssertFalse(graph.areNodesConnected(source: 5, destination: i))
}
}
}