├── .envrc ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── flake.lock ├── flake.nix ├── nix ├── devshell.nix └── overlay.nix ├── shell.nix └── src ├── graph.zig └── tarjan.zig /.envrc: -------------------------------------------------------------------------------- 1 | # If we are a computer with nix-shell available, then use that to setup 2 | # the build environment with exactly what we need. 3 | if has nix-shell; then 4 | use nix 5 | fi 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-graph 2 | 3 | A Zig library for directed graph data structures and associated algorithms. 4 | This library can be used for acyclic and cyclic graphs and unweighted and weighted 5 | edges. This library requires Zig 0.9+. 6 | 7 | **Warning: This is literally the first piece of Zig code I've ever written 8 | in my life.** I'm using this project as a way to learn how to do things in 9 | Zig, what is idiomatic, what isn't, etc. Feedback is very welcome on how 10 | I can improve and I expect to alter the library a bit as I do so. There is 11 | also a lot of room for improvement in performance by various measures. 12 | 13 | ## Features 14 | 15 | * Directed edges 16 | * Cycle detection 17 | * Strongly connected components 18 | * Cheap edge reversal 19 | * Depth-first traversal 20 | 21 | #### TODO 22 | 23 | * Vertex iterator 24 | * Edge iterator 25 | * Dijkstra for single-source shortest path w/ edge-weighting 26 | * Kahn for topological sorting 27 | * Shortest path given a topological sort 28 | * String marshaling for easier debugging 29 | * "Unmanaged" graph so allocator can be sent to each op 30 | 31 | ## Example 32 | 33 | ```zig 34 | const std = @import("std"); 35 | const graph = @import("graph"); 36 | 37 | pub fn main() void { 38 | // Create a directed graph type for strings. 39 | const Graph = graph.DirectedGraph([]const u8, std.hash_map.StringContext); 40 | 41 | // Initialize using some allocator 42 | var g = Graph.init(std.debug.global_allocator); 43 | defer g.deinit(); 44 | 45 | // Add some vertices 46 | try g.add("A"); 47 | try g.add("B"); 48 | try g.add("C"); 49 | 50 | // Add some edges with weights. For unweighted edges just make all 51 | // weights the same value. 52 | try g.addEdge("A", "B", 5); 53 | try g.addEdge("A", "C", 2); 54 | try g.addEdge("B", "C", 2); 55 | try g.addEdge("C", "B", 3); 56 | 57 | // We can detect cycles 58 | if (g.cycles()) |cycles| { 59 | defer cycles.deinit(); 60 | std.log.info("there are {d} cycles", .{cycles.count()}); 61 | return; 62 | } 63 | 64 | // We can do a depth-first search through iteration. 65 | var dfsIter = try g.dfsIterator("B"); 66 | while (dfsIter.next()) |id| { 67 | std.log.info("{}", .{g.lookup(id).?}); 68 | } 69 | dfsIter.deinit(); 70 | 71 | // We can easily reverse the graph if we want. 72 | const reversed = g.reverse(); 73 | 74 | // ... and more 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | 3 | pub fn build(b: *Builder) void { 4 | const mode = b.standardReleaseOptions(); 5 | const lib = b.addStaticLibrary("graph", "src/graph.zig"); 6 | lib.setBuildMode(mode); 7 | 8 | var main_tests = b.addTest("src/graph.zig"); 9 | main_tests.setBuildMode(mode); 10 | 11 | const test_step = b.step("test", "Run library tests"); 12 | test_step.dependOn(&main_tests.step); 13 | 14 | b.default_step.dependOn(&lib.step); 15 | b.installArtifact(lib); 16 | } 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1638122382, 6 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils_2": { 19 | "locked": { 20 | "lastModified": 1629481132, 21 | "narHash": "sha256-JHgasjPR0/J1J3DRm4KxM4zTyAj4IOJY8vIl75v/kPI=", 22 | "owner": "numtide", 23 | "repo": "flake-utils", 24 | "rev": "997f7efcb746a9c140ce1f13c72263189225f482", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "type": "github" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "locked": { 35 | "lastModified": 1639713555, 36 | "narHash": "sha256-w1TacWjnqhC19n+rheyOif3JxwvWMbyxfgqYCY0FLdQ=", 37 | "owner": "nixos", 38 | "repo": "nixpkgs", 39 | "rev": "45a3f9d7725c7e21b252c223676cc56fb2ed5d6d", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "nixos", 44 | "ref": "nixpkgs-unstable", 45 | "repo": "nixpkgs", 46 | "type": "github" 47 | } 48 | }, 49 | "nixpkgs_2": { 50 | "locked": { 51 | "lastModified": 1631288242, 52 | "narHash": "sha256-sXm4KiKs7qSIf5oTAmrlsEvBW193sFj+tKYVirBaXz0=", 53 | "owner": "NixOS", 54 | "repo": "nixpkgs", 55 | "rev": "0e24c87754430cb6ad2f8c8c8021b29834a8845e", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "NixOS", 60 | "ref": "nixpkgs-unstable", 61 | "repo": "nixpkgs", 62 | "type": "github" 63 | } 64 | }, 65 | "root": { 66 | "inputs": { 67 | "flake-utils": "flake-utils", 68 | "nixpkgs": "nixpkgs", 69 | "zig": "zig" 70 | } 71 | }, 72 | "zig": { 73 | "inputs": { 74 | "flake-utils": "flake-utils_2", 75 | "nixpkgs": "nixpkgs_2" 76 | }, 77 | "locked": { 78 | "lastModified": 1639700903, 79 | "narHash": "sha256-BTA5j/KYSI/x2MUOlvoJ0lvX1b9A95UePYoGuj4NGNk=", 80 | "owner": "arqv", 81 | "repo": "zig-overlay", 82 | "rev": "6578166149ee345baca797f313c1b18d587ad252", 83 | "type": "github" 84 | }, 85 | "original": { 86 | "owner": "arqv", 87 | "repo": "zig-overlay", 88 | "type": "github" 89 | } 90 | } 91 | }, 92 | "root": "root", 93 | "version": 7 94 | } 95 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "zig-graph"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | zig.url = "github:arqv/zig-overlay"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, ... }@inputs: 11 | # These are the same systems that zig supports 12 | let systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 13 | in flake-utils.lib.eachSystem systems (system: 14 | let 15 | # Our in-repo overlay of packages 16 | overlay = (import ./nix/overlay.nix) { 17 | inherit nixpkgs; 18 | zigpkgs = inputs.zig.packages.${system}; 19 | }; 20 | 21 | # Initialize our package repository, adding overlays from inputs 22 | pkgs = import nixpkgs { 23 | inherit system; 24 | 25 | overlays = [ 26 | overlay 27 | ]; 28 | }; 29 | in rec { 30 | devShell = pkgs.devShell; 31 | } 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /nix/devshell.nix: -------------------------------------------------------------------------------- 1 | { mkShell 2 | , zig 3 | }: mkShell rec { 4 | name = "zig-graph"; 5 | 6 | buildInputs = [ 7 | zig 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs 2 | , zigpkgs }: final: prev: rec { 3 | # Notes: 4 | # 5 | # When determining a SHA256, use this to set a fake one until we know 6 | # the real value: 7 | # 8 | # vendorSha256 = nixpkgs.lib.fakeSha256; 9 | # 10 | 11 | devShell = prev.callPackage ./devshell.nix { }; 12 | 13 | # zig we want to be the latest nightly since 0.9.0 is not released yet. 14 | zig = zigpkgs.master.latest; 15 | } 16 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) { 3 | src = builtins.fetchGit ./.; 4 | } 5 | ).shellNix 6 | -------------------------------------------------------------------------------- /src/graph.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const hash_map = std.hash_map; 3 | const math = std.math; 4 | const testing = std.testing; 5 | const Allocator = std.mem.Allocator; 6 | const tarjan = @import("tarjan.zig"); 7 | 8 | pub const GraphError = error{ 9 | VertexNotFoundError, 10 | }; 11 | 12 | /// A directed graph that contains nodes of a given type. 13 | /// 14 | /// The Context is the same as the Context for std.hash_map and must 15 | /// provide for a hash function and equality function. This is used to 16 | /// determine graph node equality. 17 | pub fn DirectedGraph( 18 | comptime T: type, 19 | comptime Context: type, 20 | ) type { 21 | // This verifies the context has the correct functions (hash and eql) 22 | comptime hash_map.verifyContext(Context, T, T, u64, false); 23 | 24 | // The adjacency list type is used to map all edges in the graph. 25 | // The key is the source node. The value is a map where the key is 26 | // target node and the value is the edge weight. 27 | const AdjMapValue = hash_map.AutoHashMap(u64, u64); 28 | const AdjMap = hash_map.AutoHashMap(u64, AdjMapValue); 29 | 30 | // ValueMap maps hash codes to the actual value. 31 | const ValueMap = hash_map.AutoHashMap(u64, T); 32 | 33 | return struct { 34 | // allocator to use for all operations 35 | allocator: Allocator, 36 | 37 | // ctx is the context implementation 38 | ctx: Context, 39 | 40 | // adjacency lists for outbound and inbound edges and a map to 41 | // get the real value. 42 | adjOut: AdjMap, 43 | adjIn: AdjMap, 44 | values: ValueMap, 45 | 46 | const Self = @This(); 47 | 48 | /// Size is the maximum size (as a type) that the graph can hold. 49 | /// This is currently dictated by our usage of HashMap underneath. 50 | const Size = AdjMap.Size; 51 | 52 | /// initialize a new directed graph. This is used if the Context type 53 | /// has no data (zero-sized). 54 | pub fn init(allocator: Allocator) Self { 55 | if (@sizeOf(Context) != 0) { 56 | @compileError("Context is non-zero sized. Use initContext instead."); 57 | } 58 | 59 | return initContext(allocator, undefined); 60 | } 61 | 62 | /// same as init but for non-zero-sized contexts. 63 | pub fn initContext(allocator: Allocator, ctx: Context) Self { 64 | return .{ 65 | .allocator = allocator, 66 | .ctx = ctx, 67 | .adjOut = AdjMap.init(allocator), 68 | .adjIn = AdjMap.init(allocator), 69 | .values = ValueMap.init(allocator), 70 | }; 71 | } 72 | /// deinitialize all the memory associated with the graph. If you 73 | /// deinitialize the allocator used with this graph you don't need to 74 | /// call this. 75 | pub fn deinit(self: *Self) void { 76 | // Free values for our adj maps 77 | var it = self.adjOut.iterator(); 78 | while (it.next()) |kv| { 79 | kv.value_ptr.deinit(); 80 | } 81 | it = self.adjIn.iterator(); 82 | while (it.next()) |kv| { 83 | kv.value_ptr.deinit(); 84 | } 85 | 86 | self.adjOut.deinit(); 87 | self.adjIn.deinit(); 88 | self.values.deinit(); 89 | self.* = undefined; 90 | } 91 | 92 | /// Add a node to the graph. 93 | pub fn add(self: *Self, v: T) !void { 94 | const h = self.ctx.hash(v); 95 | 96 | // If we already have this node, then do nothing. 97 | if (self.adjOut.contains(h)) { 98 | return; 99 | } 100 | 101 | try self.adjOut.put(h, AdjMapValue.init(self.allocator)); 102 | try self.adjIn.put(h, AdjMapValue.init(self.allocator)); 103 | try self.values.put(h, v); 104 | } 105 | 106 | /// Remove a node and all edges to and from the node. 107 | pub fn remove(self: *Self, v: T) void { 108 | const h = self.ctx.hash(v); 109 | 110 | // Forget this value 111 | _ = self.values.remove(h); 112 | 113 | // Delete in-edges for this vertex. 114 | if (self.adjOut.getPtr(h)) |map| { 115 | var it = map.iterator(); 116 | while (it.next()) |kv| { 117 | if (self.adjIn.getPtr(kv.key_ptr.*)) |inMap| { 118 | _ = inMap.remove(h); 119 | } 120 | } 121 | 122 | map.deinit(); 123 | _ = self.adjOut.remove(h); 124 | } 125 | 126 | // Delete out-edges for this vertex 127 | if (self.adjIn.getPtr(h)) |map| { 128 | var it = map.iterator(); 129 | while (it.next()) |kv| { 130 | if (self.adjOut.getPtr(kv.key_ptr.*)) |inMap| { 131 | _ = inMap.remove(h); 132 | } 133 | } 134 | 135 | map.deinit(); 136 | _ = self.adjIn.remove(h); 137 | } 138 | } 139 | 140 | /// contains returns true if the graph has the given vertex. 141 | pub fn contains(self: *Self, v: T) bool { 142 | return self.values.contains(self.ctx.hash(v)); 143 | } 144 | 145 | /// lookup looks up a vertex by hash. The hash is often used 146 | /// as a result of algorithms such as strongly connected components 147 | /// since it is easier to work with. This function can be called to 148 | /// get the real value. 149 | pub fn lookup(self: *Self, hash: u64) ?T { 150 | return self.values.get(hash); 151 | } 152 | 153 | /// add an edge from one node to another. This will return an 154 | /// error if either vertex does not exist. 155 | pub fn addEdge(self: *Self, from: T, to: T, weight: u64) !void { 156 | const h1 = self.ctx.hash(from); 157 | const h2 = self.ctx.hash(to); 158 | 159 | const mapOut = self.adjOut.getPtr(h1) orelse 160 | return GraphError.VertexNotFoundError; 161 | const mapIn = self.adjIn.getPtr(h2) orelse 162 | return GraphError.VertexNotFoundError; 163 | 164 | try mapOut.put(h2, weight); 165 | try mapIn.put(h1, weight); 166 | } 167 | 168 | /// remove an edge 169 | pub fn removeEdge(self: *Self, from: T, to: T) void { 170 | const h1 = self.ctx.hash(from); 171 | const h2 = self.ctx.hash(to); 172 | 173 | if (self.adjOut.getPtr(h1)) |map| { 174 | _ = map.remove(h2); 175 | } else unreachable; 176 | 177 | if (self.adjIn.getPtr(h2)) |map| { 178 | _ = map.remove(h1); 179 | } else unreachable; 180 | } 181 | 182 | /// getEdge gets the edge from one node to another and returns the 183 | /// weight, if it exists. 184 | pub fn getEdge(self: *const Self, from: T, to: T) ?u64 { 185 | const h1 = self.ctx.hash(from); 186 | const h2 = self.ctx.hash(to); 187 | 188 | if (self.adjOut.getPtr(h1)) |map| { 189 | return map.get(h2); 190 | } else unreachable; 191 | } 192 | 193 | // reverse reverses the graph. This does NOT make any copies, so 194 | // any changes to the original affect the reverse and vice versa. 195 | // Likewise, only one of these graphs should be deinitialized. 196 | pub fn reverse(self: *const Self) Self { 197 | return Self{ 198 | .allocator = self.allocator, 199 | .ctx = self.ctx, 200 | .adjOut = self.adjIn, 201 | .adjIn = self.adjOut, 202 | .values = self.values, 203 | }; 204 | } 205 | 206 | /// Create a copy of this graph using the same allocator. 207 | pub fn clone(self: *const Self) !Self { 208 | return Self{ 209 | .allocator = self.allocator, 210 | .ctx = self.ctx, 211 | .adjOut = try cloneAdjMap(&self.adjOut), 212 | .adjIn = try cloneAdjMap(&self.adjIn), 213 | .values = try self.values.clone(), 214 | }; 215 | } 216 | 217 | /// clone our AdjMap including inner values. 218 | fn cloneAdjMap(m: *const AdjMap) !AdjMap { 219 | // Clone the outer container 220 | var new = try m.clone(); 221 | 222 | // Clone all objects 223 | var it = new.iterator(); 224 | while (it.next()) |kv| { 225 | try new.put(kv.key_ptr.*, try kv.value_ptr.clone()); 226 | } 227 | 228 | return new; 229 | } 230 | 231 | /// The number of vertices in the graph. 232 | pub fn countVertices(self: *const Self) Size { 233 | return self.values.count(); 234 | } 235 | 236 | /// The number of edges in the graph. 237 | /// 238 | /// O(V) where V is the # of vertices. We could cache this if we 239 | /// wanted but its not a very common operation. 240 | pub fn countEdges(self: *const Self) Size { 241 | var count: Size = 0; 242 | var it = self.adjOut.iterator(); 243 | while (it.next()) |kv| { 244 | count += kv.value_ptr.count(); 245 | } 246 | 247 | return count; 248 | } 249 | 250 | /// Cycles returns the set of cycles (if any). 251 | pub fn cycles( 252 | self: *const Self, 253 | ) ?tarjan.StronglyConnectedComponents { 254 | var sccs = self.stronglyConnectedComponents(); 255 | var i: usize = 0; 256 | while (i < sccs.list.items.len) { 257 | const current = sccs.list.items[i]; 258 | if (current.items.len <= 1) { 259 | const old = sccs.list.swapRemove(i); 260 | old.deinit(); 261 | continue; 262 | } 263 | 264 | i += 1; 265 | } 266 | 267 | if (sccs.list.items.len == 0) { 268 | sccs.deinit(); 269 | return null; 270 | } 271 | 272 | return sccs; 273 | } 274 | 275 | /// Returns the set of strongly connected components in this graph. 276 | /// This allocates memory. 277 | pub fn stronglyConnectedComponents( 278 | self: *const Self, 279 | ) tarjan.StronglyConnectedComponents { 280 | return tarjan.stronglyConnectedComponents(self.allocator, self); 281 | } 282 | 283 | /// dfsIterator returns an iterator that iterates all reachable 284 | /// vertices from "start". Note that the DFSIterator must have 285 | /// deinit called. It is an error if start does not exist. 286 | pub fn dfsIterator(self: *const Self, start: T) !DFSIterator { 287 | const h = self.ctx.hash(start); 288 | 289 | // Start must exist 290 | if (!self.values.contains(h)) { 291 | return GraphError.VertexNotFoundError; 292 | } 293 | 294 | // We could pre-allocate some space here and assume we'll visit 295 | // the full graph or something. Keeping it simple for now. 296 | var stack = std.ArrayList(u64).init(self.allocator); 297 | var visited = std.AutoHashMap(u64, void).init(self.allocator); 298 | 299 | return DFSIterator{ 300 | .g = self, 301 | .stack = stack, 302 | .visited = visited, 303 | .current = h, 304 | }; 305 | } 306 | 307 | pub const DFSIterator = struct { 308 | // Not the most efficient data structures for this, I know, 309 | // but we can come back and optimize this later since its opaque. 310 | // 311 | // stack and visited must ensure capacity 312 | g: *const Self, 313 | stack: std.ArrayList(u64), 314 | visited: std.AutoHashMap(u64, void), 315 | current: ?u64, 316 | 317 | // DFSIterator must deinit 318 | pub fn deinit(it: *DFSIterator) void { 319 | it.stack.deinit(); 320 | it.visited.deinit(); 321 | } 322 | 323 | /// next returns the list of hash IDs for the vertex. This should be 324 | /// looked up again with the graph to get the actual vertex value. 325 | pub fn next(it: *DFSIterator) !?u64 { 326 | // If we're out of values, then we're done. 327 | if (it.current == null) return null; 328 | 329 | // Our result is our current value 330 | const result = it.current orelse unreachable; 331 | try it.visited.put(result, {}); 332 | 333 | // Add all adjacent edges to the stack. We do a 334 | // visited check here to avoid revisiting vertices 335 | if (it.g.adjOut.getPtr(result)) |map| { 336 | var iter = map.keyIterator(); 337 | while (iter.next()) |target| { 338 | if (!it.visited.contains(target.*)) { 339 | try it.stack.append(target.*); 340 | } 341 | } 342 | } 343 | 344 | // Advance to the next value 345 | it.current = null; 346 | while (it.stack.popOrNull()) |nextVal| { 347 | if (!it.visited.contains(nextVal)) { 348 | it.current = nextVal; 349 | break; 350 | } 351 | } 352 | 353 | return result; 354 | } 355 | }; 356 | }; 357 | } 358 | 359 | test "add and remove vertex" { 360 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 361 | var g = gtype.init(testing.allocator); 362 | defer g.deinit(); 363 | 364 | // No vertex 365 | try testing.expect(!g.contains("A")); 366 | 367 | // Add some nodes 368 | try g.add("A"); 369 | try g.add("A"); 370 | try g.add("B"); 371 | try testing.expect(g.contains("A")); 372 | try testing.expect(g.countVertices() == 2); 373 | try testing.expect(g.countEdges() == 0); 374 | 375 | // add an edge 376 | try g.addEdge("A", "B", 1); 377 | try testing.expect(g.countEdges() == 1); 378 | 379 | // Remove a node 380 | g.remove("A"); 381 | try testing.expect(g.countVertices() == 1); 382 | 383 | // important: removing a node should remove the edge 384 | try testing.expect(g.countEdges() == 0); 385 | } 386 | 387 | test "add and remove edge" { 388 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 389 | var g = gtype.init(testing.allocator); 390 | defer g.deinit(); 391 | 392 | // Add some nodes 393 | try g.add("A"); 394 | try g.add("A"); 395 | try g.add("B"); 396 | 397 | // add an edge 398 | try g.addEdge("A", "B", 1); 399 | try g.addEdge("A", "B", 4); 400 | try testing.expect(g.countEdges() == 1); 401 | try testing.expect(g.getEdge("A", "B").? == 4); 402 | 403 | // Remove the node 404 | g.removeEdge("A", "B"); 405 | g.removeEdge("A", "B"); 406 | try testing.expect(g.countEdges() == 0); 407 | try testing.expect(g.countVertices() == 2); 408 | } 409 | 410 | test "reverse" { 411 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 412 | var g = gtype.init(testing.allocator); 413 | defer g.deinit(); 414 | 415 | // Add some nodes 416 | try g.add("A"); 417 | try g.add("B"); 418 | try g.addEdge("A", "B", 1); 419 | 420 | // Reverse 421 | const rev = g.reverse(); 422 | 423 | // Should have the same number 424 | try testing.expect(rev.countEdges() == 1); 425 | try testing.expect(rev.countVertices() == 2); 426 | try testing.expect(rev.getEdge("A", "B") == null); 427 | try testing.expect(rev.getEdge("B", "A").? == 1); 428 | } 429 | 430 | test "clone" { 431 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 432 | var g = gtype.init(testing.allocator); 433 | defer g.deinit(); 434 | 435 | // Add some nodes 436 | try g.add("A"); 437 | 438 | // Clone 439 | var g2 = try g.clone(); 440 | defer g2.deinit(); 441 | 442 | try g.add("B"); 443 | try testing.expect(g.contains("B")); 444 | try testing.expect(!g2.contains("B")); 445 | } 446 | 447 | test "cycles and strongly connected components" { 448 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 449 | var g = gtype.init(testing.allocator); 450 | defer g.deinit(); 451 | 452 | // Add some nodes 453 | try g.add("A"); 454 | var alone = g.stronglyConnectedComponents(); 455 | defer alone.deinit(); 456 | const value = g.lookup(alone.list.items[0].items[0]); 457 | try testing.expectEqual(value.?, "A"); 458 | 459 | // Add more 460 | try g.add("B"); 461 | try g.addEdge("A", "B", 1); 462 | var sccs = g.stronglyConnectedComponents(); 463 | defer sccs.deinit(); 464 | try testing.expect(sccs.count() == 2); 465 | try testing.expect(g.cycles() == null); 466 | 467 | // Add a cycle 468 | try g.addEdge("B", "A", 1); 469 | var sccs2 = g.stronglyConnectedComponents(); 470 | defer sccs2.deinit(); 471 | try testing.expect(sccs2.count() == 1); 472 | 473 | // Should have a cycle 474 | var cycles = g.cycles() orelse unreachable; 475 | defer cycles.deinit(); 476 | try testing.expect(cycles.count() == 1); 477 | } 478 | 479 | test "dfs" { 480 | const gtype = DirectedGraph([]const u8, std.hash_map.StringContext); 481 | var g = gtype.init(testing.allocator); 482 | defer g.deinit(); 483 | 484 | // Add some nodes 485 | try g.add("A"); 486 | try g.add("B"); 487 | try g.add("C"); 488 | try g.addEdge("B", "C", 1); 489 | try g.addEdge("C", "A", 1); 490 | 491 | // DFS from A should only reach A 492 | { 493 | var list = std.ArrayList([]const u8).init(testing.allocator); 494 | defer list.deinit(); 495 | var iter = try g.dfsIterator("A"); 496 | defer iter.deinit(); 497 | while (try iter.next()) |value| { 498 | try list.append(g.lookup(value).?); 499 | } 500 | 501 | const expect = [_][]const u8{"A"}; 502 | try testing.expectEqualSlices([]const u8, list.items, &expect); 503 | } 504 | 505 | // DFS from B 506 | { 507 | var list = std.ArrayList([]const u8).init(testing.allocator); 508 | defer list.deinit(); 509 | var iter = try g.dfsIterator("B"); 510 | defer iter.deinit(); 511 | while (try iter.next()) |value| { 512 | try list.append(g.lookup(value).?); 513 | } 514 | 515 | const expect = [_][]const u8{ "B", "C", "A" }; 516 | try testing.expectEqualSlices([]const u8, &expect, list.items); 517 | } 518 | } 519 | 520 | test { 521 | _ = tarjan; 522 | } 523 | -------------------------------------------------------------------------------- /src/tarjan.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = std.math; 3 | const testing = std.testing; 4 | const Allocator = std.mem.Allocator; 5 | 6 | /// A list of strongly connected components. 7 | /// 8 | /// This is effectively [][]u64 for a DirectedGraph. The u64 value is the 9 | /// hash code, NOT the type T. You should use the lookup function to get the 10 | /// actual vertex. 11 | pub const StronglyConnectedComponents = struct { 12 | const Self = @This(); 13 | const Entry = std.ArrayList(u64); 14 | const List = std.ArrayList(Entry); 15 | 16 | /// The list of components. Do not access this directly. This type 17 | /// also owns all the items, so when deinit is called, all items in this 18 | /// list will also be deinit-ed. 19 | list: List, 20 | 21 | /// Iterator is used to iterate through the strongly connected components. 22 | pub const Iterator = struct { 23 | list: *const List, 24 | index: usize = 0, 25 | 26 | /// next returns the list of hash IDs for the vertex. This should be 27 | /// looked up again with the graph to get the actual vertex value. 28 | pub fn next(it: *Iterator) ?[]u64 { 29 | // If we're empty or at the end, we're done. 30 | if (it.list.items.len == 0 or it.list.items.len <= it.index) return null; 31 | 32 | // Bump the index, return our value 33 | defer it.index += 1; 34 | return it.list.items[it.index].items; 35 | } 36 | }; 37 | 38 | pub fn init(allocator: Allocator) Self { 39 | return Self{ 40 | .list = List.init(allocator), 41 | }; 42 | } 43 | 44 | pub fn deinit(self: *Self) void { 45 | for (self.list.items) |v| { 46 | v.deinit(); 47 | } 48 | self.list.deinit(); 49 | } 50 | 51 | /// Iterate over all the strongly connected components 52 | pub fn iterator(self: *const Self) Iterator { 53 | return .{ .list = &self.list }; 54 | } 55 | 56 | /// The number of distinct strongly connected components. 57 | pub fn count(self: *const Self) usize { 58 | return self.list.items.len; 59 | } 60 | }; 61 | 62 | /// Calculate the set of strongly connected components in the graph g. 63 | /// The argument g must be a DirectedGraph type. 64 | pub fn stronglyConnectedComponents( 65 | allocator: Allocator, 66 | g: anytype, 67 | ) StronglyConnectedComponents { 68 | var acc = sccAcc.init(allocator); 69 | defer acc.deinit(); 70 | var result = StronglyConnectedComponents.init(allocator); 71 | 72 | var iter = g.values.keyIterator(); 73 | while (iter.next()) |h| { 74 | if (!acc.map.contains(h.*)) { 75 | _ = stronglyConnectedStep(allocator, g, &acc, &result, h.*); 76 | } 77 | } 78 | 79 | return result; 80 | } 81 | 82 | fn stronglyConnectedStep( 83 | allocator: Allocator, 84 | g: anytype, 85 | acc: *sccAcc, 86 | result: *StronglyConnectedComponents, 87 | current: u64, 88 | ) u32 { 89 | // TODO(mitchellh): I don't like this unreachable here. 90 | const idx = acc.visit(current) catch unreachable; 91 | var minIdx = idx; 92 | 93 | var iter = g.adjOut.getPtr(current).?.keyIterator(); 94 | while (iter.next()) |targetPtr| { 95 | const target = targetPtr.*; 96 | const targetIdx = acc.map.get(target) orelse 0; 97 | 98 | if (targetIdx == 0) { 99 | minIdx = math.min( 100 | minIdx, 101 | stronglyConnectedStep(allocator, g, acc, result, target), 102 | ); 103 | } else if (acc.inStack(target)) { 104 | minIdx = math.min(minIdx, targetIdx); 105 | } 106 | } 107 | 108 | // If this is the vertex we started with then build our result. 109 | if (idx == minIdx) { 110 | var scc = std.ArrayList(u64).init(allocator); 111 | while (true) { 112 | const v = acc.pop(); 113 | scc.append(v) catch unreachable; 114 | if (v == current) { 115 | break; 116 | } 117 | } 118 | 119 | result.list.append(scc) catch unreachable; 120 | } 121 | 122 | return minIdx; 123 | } 124 | 125 | /// Internal accumulator used to calculate the strongly connected 126 | /// components. This should not be used publicly. 127 | pub const sccAcc = struct { 128 | const MapType = std.hash_map.AutoHashMap(u64, Size); 129 | const StackType = std.ArrayList(u64); 130 | 131 | next: Size, 132 | map: MapType, 133 | stack: StackType, 134 | 135 | // Size is the maximum number of vertices that could exist. Our graph 136 | // is limited to 32 bit numbers due to the underlying usage of HashMap. 137 | const Size = u32; 138 | 139 | const Self = @This(); 140 | 141 | pub fn init(allocator: Allocator) Self { 142 | return Self{ 143 | .next = 1, 144 | .map = MapType.init(allocator), 145 | .stack = StackType.init(allocator), 146 | }; 147 | } 148 | 149 | pub fn deinit(self: *Self) void { 150 | self.map.deinit(); 151 | self.stack.deinit(); 152 | self.* = undefined; 153 | } 154 | 155 | pub fn visit(self: *Self, v: u64) !Size { 156 | const idx = self.next; 157 | try self.map.put(v, idx); 158 | self.next += 1; 159 | try self.stack.append(v); 160 | return idx; 161 | } 162 | 163 | pub fn pop(self: *Self) u64 { 164 | return self.stack.pop(); 165 | } 166 | 167 | pub fn inStack(self: *Self, v: u64) bool { 168 | for (self.stack.items) |i| { 169 | if (i == v) { 170 | return true; 171 | } 172 | } 173 | 174 | return false; 175 | } 176 | }; 177 | 178 | test "sccAcc" { 179 | var acc = sccAcc.init(testing.allocator); 180 | defer acc.deinit(); 181 | 182 | // should start at nothing 183 | try testing.expect(acc.next == 1); 184 | try testing.expect(!acc.inStack(42)); 185 | 186 | // add vertex 187 | try testing.expect((try acc.visit(42)) == 1); 188 | try testing.expect(acc.next == 2); 189 | try testing.expect(acc.inStack(42)); 190 | 191 | const v = acc.pop(); 192 | try testing.expect(v == 42); 193 | } 194 | 195 | test "StronglyConnectedComponents" { 196 | var sccs = StronglyConnectedComponents.init(testing.allocator); 197 | defer sccs.deinit(); 198 | 199 | // Initially empty 200 | try testing.expect(sccs.count() == 0); 201 | 202 | // Build our entries 203 | var entries = StronglyConnectedComponents.Entry.init(testing.allocator); 204 | try entries.append(1); 205 | try entries.append(2); 206 | try entries.append(3); 207 | try sccs.list.append(entries); 208 | 209 | // Should have one 210 | try testing.expect(sccs.count() == 1); 211 | 212 | // Test iteration 213 | var iter = sccs.iterator(); 214 | var count: u8 = 0; 215 | while (iter.next()) |set| { 216 | const expect = [_]u64{ 1, 2, 3 }; 217 | try testing.expectEqual(set.len, 3); 218 | try testing.expectEqualSlices(u64, set, &expect); 219 | count += 1; 220 | } 221 | try testing.expect(count == 1); 222 | } 223 | --------------------------------------------------------------------------------