const std = @import("std"); const example = \\start-A \\start-b \\A-c \\A-b \\b-d \\A-end \\b-end ; const example2 = \\dc-end \\HN-start \\start-kj \\dc-start \\dc-HN \\LN-dc \\HN-end \\kj-sa \\kj-HN \\kj-dc ; const example3 = \\fs-end \\he-DX \\fs-he \\start-DX \\pj-DX \\end-zg \\zg-sl \\zg-pj \\pj-he \\RW-he \\fs-DX \\pj-RW \\zg-RW \\start-pj \\he-WI \\zg-he \\pj-fs \\start-RW ; const input = @embedFile("../input"); const Graph = struct { alloc: std.mem.Allocator, nodes: std.ArrayListUnmanaged(*Node) = .{}, const Self = @This(); pub fn deinit(self: *Self) void { for (self.nodes.items) |n| { n.parents.deinit(self.alloc); n.children.deinit(self.alloc); self.alloc.destroy(n); } self.nodes.deinit(self.alloc); } pub fn build_graph(alloc: std.mem.Allocator, text: []const u8) !Self { var self: Self = .{ .alloc = alloc, }; var start = try alloc.create(Node); errdefer alloc.destroy(start); start.id = "start"; start.children = .{}; start.parents = .{}; var end = try alloc.create(Node); errdefer alloc.destroy(end); end.id = "end"; end.children = .{}; end.parents = .{}; // Our input text does not necessarrily contain the Nodes in an order // that will just assemble into a graph. We temporarily hold an array // of pointers to all the nodes we're making to facilitate building // the graph. try self.nodes.append(self.alloc, start); try self.nodes.append(self.alloc, end); var lit = std.mem.tokenize(u8, text, "\n"); while (lit.next()) |line| { var it = std.mem.tokenize(u8, line, "-"); var first: ?*Node = null; var second: ?*Node = null; while (it.next()) |id| { var n = self.node_exists(id); if (n == null) { n = try alloc.create(Node); errdefer alloc.destroy(n.?); n.?.id = id; var big = true; for (id) |c| { if (!std.ascii.isUpper(c)) { big = false; break; } } n.?.big = big; n.?.children = .{}; n.?.parents = .{}; try self.nodes.append(self.alloc, n.?); } if (first == null) { first = n; } else if (second == null) { second = n; } } // @LEAK When there is an error, the unmanaged array lists aren't properly freed //std.log.debug("Adding '{s}' as parent of '{s}'", .{first.?.id, second.?.id}); try second.?.parents.append(self.alloc, first.?); try first.?.children.append(self.alloc, second.?); } //std.debug.assert(start.parent == null); //std.debug.assert(end.parent != null); return self; } pub fn paths_part1(self: *Self) !Paths { var paths = try self.node_exists("start").?.walk(alloc, null); return paths; } pub fn node_exists(self: *Self, id: []const u8) ?*Node { for (self.nodes.items) |n| { if (std.mem.eql(u8, id, n.id)) { return n; } } return null; } pub fn to_dot(self: *Self, f: std.fs.File) !void { defer f.close(); _ = try f.writeAll("digraph D {\n"); for (self.nodes.items) |n| { _ = try f.write(n.id); _ = try f.write(" [shape=box]\n"); } for(self.nodes.items) |n| { if (n.children.items.len > 0) { _ = try f.write(n.id); _ = try f.write(" -> {"); for (n.children.items) |c, k| { if (std.mem.eql(u8, c.id, n.id)) { continue; } _ = try f.write(c.id); if (k != n.children.items.len - 1) { _ = try f.write(", "); } } _ = try f.write("}\n"); } if (n.parents.items.len > 0) { _ = try f.write(n.id); _ = try f.write(" -> {"); for (n.parents.items) |c, k| { if (std.mem.eql(u8, c.id, n.id)) { continue; } _ = try f.write(c.id); if (k != n.parents.items.len - 1) { _ = try f.write(", "); } } _ = try f.write("}\n"); } } try f.writeAll("}\n"); } }; const Path = std.ArrayListUnmanaged(*Node); const Paths = struct { alloc: std.mem.Allocator, paths: std.ArrayListUnmanaged(*Path), const Self = @This(), pub fn init(alloc: std.mem.Allocator) !Self { return Self { .alloc = alloc, .paths = .{} }; } } const Node = struct { id: [] const u8, big: bool = false, parents: std.ArrayListUnmanaged(*Node) = .{}, children: std.ArrayListUnmanaged(*Node) = .{}, const Self = @This(); fn valid_child_part1(self: *Self, path: std.ArrayList(*Node)) bool { if (self.big) { return true; } for (path.items) |p| { if (std.mem.eql(u8, p.id, self.id)) { return false; } } return true; } pub fn walk(self: *Self, alloc: std.mem.Allocator, visited: ?Path) !Paths { var paths = Path.init(alloc); // Depth first if (visited == null) { var new_path = try alloc.create(Path); errdefer alloc.destroy(new_path); try new_path.append(self); } else { try visited.append(alloc, self); } return paths; } // pub fn paths(self: *Self, alloc: std.mem.Allocator) !std.ArrayList(*Path) { // var ps = std.ArrayList(*Path).init(alloc); // if (ps.items.len == 0) { // var path = try alloc.create(Path); // errdefer alloc.destroy(path); // path.* = Path.init(alloc); // try ps.append(path); // } // for (ps.items) |path| { // try path.append(self); // } // if (std.mem.eql(u8, self.id, "end")) { // return ps; // } // return ps; // } // pub fn paths_from(self: *Self, alloc: std.mem.Allocator, from: Path) !std.ArrayList(Path) { // var ps = std.ArrayList(*Path).init(alloc); // for (self.children.items) |c| { // if (!c.valid_child_part1(from)) { // continue; // } // var path = alloc.create(std.ArrayList(Path)); // path.* = std.ArrayList(Path).init(alloc); // for (from.items) |f| { // try path.append(f); // } // try path.append(c); // try ps.append(path); // } // return ps; // } }; test "example 1" { var graph = try Graph.build_graph(std.testing.allocator, example); defer graph.deinit(); var end = graph.node_exists("end"); var end_paths = try end.?.paths(std.testing.allocator); for (end_paths.items) |p, k| { std.log.warn("Path number {}", .{k}); for (p.items) |n| { std.log.warn("\t{s}", .{n.id}); } } // Cleanup for (end_paths.items) |p| { p.deinit(); std.testing.allocator.destroy(p); } end_paths.deinit(); var paths = try graph.paths_part1(); for (paths.items) |p, k| { std.log.warn("Path number {}", .{k}); for (p.items) |n| { std.log.warn("\t{s}", .{n.id}); } } for (paths.items) |p| { p.deinit(); std.testing.allocator.destroy(p); } paths.deinit(); } test "example 1 graph" { var graph = try Graph.build_graph(std.testing.allocator, example); defer graph.deinit(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile("example1.dot", .{}); // this closes the file descriptor. try graph.to_dot(file); var expected = @embedFile("../examples/1.dot"); var f = try tmp.dir.openFile("example1.dot", .{}); defer f.close(); const actual = try f.readToEndAlloc(std.testing.allocator, std.math.maxInt(u16)); defer std.testing.allocator.free(actual); try std.testing.expect(std.mem.eql(u8, expected, actual)); } test "example 2 graph" { var graph = try Graph.build_graph(std.testing.allocator, example2); defer graph.deinit(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile("example2.dot", .{}); // this closes the file descriptor. try graph.to_dot(file); var expected = @embedFile("../examples/2.dot"); var f = try tmp.dir.openFile("example2.dot", .{}); defer f.close(); const actual = try f.readToEndAlloc(std.testing.allocator, std.math.maxInt(u16)); defer std.testing.allocator.free(actual); try std.testing.expect(std.mem.eql(u8, expected, actual)); } test "example 3 graph" { var graph = try Graph.build_graph(std.testing.allocator, example3); defer graph.deinit(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile("example3.dot", .{}); // this closes the file descriptor. try graph.to_dot(file); var expected = @embedFile("../examples/3.dot"); var f = try tmp.dir.openFile("example3.dot", .{}); defer f.close(); const actual = try f.readToEndAlloc(std.testing.allocator, std.math.maxInt(u16)); defer std.testing.allocator.free(actual); try std.testing.expect(std.mem.eql(u8, expected, actual)); } pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); // Read our input var graph = try Graph.build_graph(alloc, input); defer graph.deinit(); var f = try std.fs.cwd().createFile("graph.dot", .{}); try graph.to_dot(f); }