From 01a2b432a6918f9145c27592a98ab392a886ebfd Mon Sep 17 00:00:00 2001 From: Hanson Char Date: Tue, 14 Jan 2025 08:51:43 -0800 Subject: [PATCH] Enhance debugging functions of Graph.lua --- learning-lua/algo/Dijkstra-test.lua | 38 +++++++++++++++ learning-lua/algo/Dijkstra.lua | 8 ++-- learning-lua/algo/Graph-test.lua | 71 ++++++++++++++++++++--------- learning-lua/algo/Graph.lua | 68 +++++++++++++++++++++------ 4 files changed, 146 insertions(+), 39 deletions(-) diff --git a/learning-lua/algo/Dijkstra-test.lua b/learning-lua/algo/Dijkstra-test.lua index 95aeeb7..19dc314 100644 --- a/learning-lua/algo/Dijkstra-test.lua +++ b/learning-lua/algo/Dijkstra-test.lua @@ -180,11 +180,49 @@ local function negative_tests() assert(string.match(errmsg, "Weight must not be negative$")) end +-- CE 191 - CEE Systems Analysis by Processor Scott Moura, University of California, Berkeley +local function scott_moura_test() + print("scott_moura_test...") + local input = {'A-B=2', 'A-C=4', 'A-D=4', 'B-F=6', 'C-F=5', 'C-E=7', 'C-D=1', 'D-E=5', 'D-H=11', 'E-G=3', + 'E-H=4', 'F-H=5', 'F-G=2', 'F-E=1', 'G-H=2'} + local source = 'A' + + local G = load_input(input) + local d = Dijkstra:new(G, source) + local sssp = d:shortest_paths() -- shortest path to all vertices + sssp:dump() + + assert(sssp:shortest_path_of('A') == 'A') + assert(sssp:min_cost(source) == 0) + + assert(sssp:shortest_path_of('B') == 'A-B') + assert(sssp:min_cost('B') == 2) + + assert(sssp:shortest_path_of('C') == 'A-C') + assert(sssp:min_cost('C') == 4) + + assert(sssp:shortest_path_of('D') == 'A-D') + assert(sssp:min_cost('D') == 4) + + assert(sssp:shortest_path_of('E') == 'A-D-E') + assert(sssp:min_cost('E') == 9) + + assert(sssp:shortest_path_of('F') == 'A-B-F') + assert(sssp:min_cost('F') == 8) + + assert(sssp:shortest_path_of('G') == 'A-B-F-G') + assert(sssp:min_cost('G') == 10) + + assert(sssp:shortest_path_of('H') == 'A-B-F-G-H') + assert(sssp:min_cost('H') == 12) +end + basic_tests() negative_tests() tim_test() geek_test() redblobgames_test() algodaily_test() +scott_moura_test() os.exit() diff --git a/learning-lua/algo/Dijkstra.lua b/learning-lua/algo/Dijkstra.lua index 0426413..f95e5e0 100644 --- a/learning-lua/algo/Dijkstra.lua +++ b/learning-lua/algo/Dijkstra.lua @@ -15,9 +15,9 @@ function Dijkstra:shortest_paths(Q) if u == Q then break -- if we are only interested in the shortest path to Q end - local edges = G:edges(u) - if edges then - for v, weight in edges:outgoings() do + local vertex = G:vertex(u) + if vertex then + for v, weight in vertex:outgoings() do assert(weight >= 0, "Weight must not be negative") local v_info, v_cost_so_far = sssp.vertices[v], sssp.vertices[u].min_cost + weight if v_cost_so_far < v_info.min_cost then @@ -90,7 +90,7 @@ function Dijkstra:new(G, s) assert(G, "Missing Graph") assert(Graph.isGraph(G), "G must be a graph object") assert(s, "Missing starting vertex") - assert(G:edges(s), "Starting vertex with at least one outgoing edge not found in graph") + assert(G:vertex(s), "Starting vertex with at least one outgoing edge not found in graph") return self:class{ graph = G, starting_vertex = s, diff --git a/learning-lua/algo/Graph-test.lua b/learning-lua/algo/Graph-test.lua index 8533bc6..c426358 100644 --- a/learning-lua/algo/Graph-test.lua +++ b/learning-lua/algo/Graph-test.lua @@ -1,35 +1,64 @@ local Graph = require "algo.Graph" -local G = Graph:new() +local function load_input(input) + local G = Graph:new() + for _, edge in ipairs(input) do + local from, to, cost = edge:match('(%w-)%-(%w+)=(%d+)') + G:add(from, to, cost) + end + return G +end -G:dump('x') +local function basic_tests() + print("basic_tests...") + local G = Graph:new() -G:add('s', 'v', 1) -G:add('s', 'w', 4) -G:add('v', 'w', 2) + print(G:outgoing_str('x')) -G:dump() + G:add('s', 'v', 1) + G:add('s', 'w', 4) + G:add('v', 'w', 2) -local edges = G:edges('s') -for v, weight in edges:outgoings() do - print('s', v, weight) -end + print(G:outgoing_str()) + + local s = G:vertex('s') + for v, weight in s:outgoings() do + print('s', v, weight) + end + + print(G:outgoing_str('s')) -G:dump('s') + local x = {} + setmetatable(x, x) + assert(not Graph.isGraph(x)) -- self reference metatable -local x = {} -setmetatable(x, x) -assert(not Graph.isGraph(x)) -- self reference metatable + assert(Graph.isGraph(G)) -- the metatable of G is Graph + assert(not Graph.isGraph {}) -- no metatable -assert(Graph.isGraph(G)) -- the metatable of G is Graph -assert(not Graph.isGraph{}) -- no metatable + local child = G:new() -- the metatable of the metatable of child is Graph + assert(Graph.isGraph(child)) -local child = G:new() -- the metatable of the metatable of child is Graph -assert(Graph.isGraph(child)) + assert(not Graph.isGraph(1)) + assert(not Graph.isGraph("")) -assert(not Graph.isGraph(1)) -assert(not Graph.isGraph("")) + assert(not Graph.isGraph()) +end + +local function test_build_ingress() + print("test_build_ingress ... ") + local input = {'A-B=2', 'A-C=4', 'A-D=4', 'B-F=6', 'C-F=5', 'C-E=7', 'C-D=1', 'D-E=5', 'D-H=11', 'E-G=3', + 'E-H=4', 'F-H=5', 'F-G=2', 'F-E=1', 'G-H=2'} + local source = 'A' + local G = load_input(input) + G:build_ingress() + local incoming, count_i = G:incoming_str() + -- print(incoming) + local outgoing, count_e = G:outgoing_str() + -- print(outgoing) + assert(count_e == count_i) +end -assert(not Graph.isGraph()) +basic_tests() +test_build_ingress() os.exit() diff --git a/learning-lua/algo/Graph.lua b/learning-lua/algo/Graph.lua index baadc7e..754fdfa 100644 --- a/learning-lua/algo/Graph.lua +++ b/learning-lua/algo/Graph.lua @@ -10,6 +10,9 @@ local mt_vertex = {} function mt_vertex:outgoings() return pairs(self.egress and self.egress or E) end +function mt_vertex:incomings() + return pairs(self.ingress and self.ingress or E) +end mt_vertex.__index = mt_vertex --- Adds a vertex u and optionally a weighted directional edge from u to v. @@ -31,45 +34,82 @@ function Graph:add(u, v, weight) end end +function Graph:build_ingress() + for u, u_node in pairs(self) do + for v, weight in pairs(u_node.egress or E) do + local v_node = self[v] + v_node.ingress = v_node.ingress or {} + v_node.ingress[u] = weight + end + end +end + function Graph:vertices() return pairs(self) end ---@param v (string) -function Graph:edges(v) +function Graph:vertex(v) return self[v] end ---@param u (string) ---@param vertex_u (table) ---@param tostring_f (function) apply this function to u for display purposes ----@return (number) the number of edges printed -local function print_outgoings(u, vertex_u, tostring_f) +---@return (table) an array of all the outgoing edges of u for debugging purposes +local function outgoing_str_of(u, vertex_u, tostring_f) + local a = {} local u_str = tostring_f and tostring_f(u) or u - local count = 0 for v, weight in vertex_u:outgoings() do local v_str = tostring_f and tostring_f(v) or v - print(string.format("%s->%s:%d", u_str, v_str, weight)) - count = count + 1 + a[#a + 1] = string.format("%s->%s:%d", u_str, v_str, weight) end - return count + return a end ----@param u (string) vertex +---@param u (string) vertex u or nil for all vertices ---@param tostring_f (function) apply this function to u for display purposes ----@return (number) the number of edges dumped -function Graph:dump(u, tostring_f) - local count = 0 +---@return (string), (number) a string of all the outgoing edges (of either vertex u if specified or of all vertices otherwise) for debugging purposes, and a count of the edges. +function Graph:outgoing_str(u, tostring_f) + local a = {} if u then if self[u] then - count = count + print_outgoings(u, self[u], tostring_f) + local from = outgoing_str_of(u, self[u], tostring_f) + table.move(from, 1, #from, #a + 1, a) end else for u, vertex_u in self:vertices() do - count = count + print_outgoings(u, vertex_u, tostring_f) + local from = outgoing_str_of(u, vertex_u, tostring_f) + table.move(from, 1, #from, #a + 1, a) + end + end + return table.concat(a, ","), #a +end + +local function incoming_str_of(v, vertex_v, tostring_f) + local a = {} + local v_str = tostring_f and tostring_f(v) or v + for u, weight in vertex_v:incomings() do + local u_str = tostring_f and tostring_f(u) or u + a[#a + 1] = string.format("%s->%s:%d", u_str, v_str, weight) + end + return a +end + +function Graph:incoming_str(v, tostring_f) + local a = {} + if v then + if self[v] then + local from = incoming_str_of(v, self[v], tostring_f) + table.move(from, 1, #from, #a + 1, a) + end + else + for v, vertex_v in self:vertices() do + local from = incoming_str_of(v, vertex_v, tostring_f) + table.move(from, 1, #from, #a + 1, a) end end - return count + return table.concat(a, ","), #a end function Graph.isGraph(o)