Blue3 port#12
Conversation
Bellman FordThe Bellman Ford algorithm finds the shortest distance paths from a particular node to all others in a directed graph. Suppose we have a graph with the following edges: let edges =
[ ('a', '0', 3)
; ('0', 'a', -1)
; ('z', '0', 5)
; ('z', 'a', 5)
]It looks something like this: graph LR
na["a"] -->|"3"| n0["0"]
n0 -->|"-1"| na["a"]
nz["z"] -->|"5"| n0["0"]
nz -->|"5"| na["a"]
If we wanted to find the shortest distance paths from print_bellman_ford ~label:"OK cycle" ~src:'z' edgesNo negative cycle found.
dist(z) = 0
dist(a) = 4
dist(0) = 5The shortest distance path from The shortest distance path from If we jumped from But if we changed the edge from let edges =
[ ('a', '0', -6)
; ('0', 'a', -1)
; ('z', '0', 5)
; ('z', 'a', 5)
]
in
print_bellman_ford ~label:"Negative Cycle" ~src:'z' edges;graph LR
na["a"] -->|"-6"| n0["0"]
n0 -->|"-1"| na["a"]
nz["z"] -->|"5"| n0["0"]
nz -->|"5"| na["a"]
...then Bellman Ford will tell us: After going from So there is no shortest path from Core Relaxation LoopBellman Ford is able to tell us all this about our graphs rather elegantly. The main idea is to iterate over the edges and "relax" the distance state at each iteration. At each iteration at some edge let relax_edge (dist : tbl) (was_updated : bool) (edge : Node.t edge) : bool =
let from_, to_, cost = edge in
match Hashtbl.find dist from_, Hashtbl.find dist to_ with
...If our current distance to Our distance table implementation uses an option to represent the distance state, with let relax_edge (dist : tbl) (was_updated : bool) (edge : Node.t edge) : bool =
let from_, to_, cost = edge in
match Hashtbl.find tbl from_, Hashtbl.find tbl to_ with
| (Some du, _), (None, _) -> ...
| (Some du, _), (Some dv, _) when du + cost < dv -> ...For a graph with let relax_edges (edges : Node.t edge list) (dist : tbl) (i : int)
: [ `Continue of tbl | `Stop of tbl ] =
let num_nodes = Hashtbl.length dist in
if i >= num_nodes - 1 then `Stop dist
...A common optimization is to early return when no distances are updated in some iteration. We can implement this using a If we relax at least one distance at any point during the edges iteration, we return let relax_edge (dist : tbl) (was_updated : bool) (edge : Node.t edge) : bool =
...
match Hashtbl.find dist from_, Hashtbl.find dist to_ with
| (Some du, _), (None, _) ->
...
was_updated || true
| (Some du, _), (Some dv, _) when du + cost < dv ->
...
was_updated || trueOtherwise we just let relax_edge (dist : tbl) (was_updated : bool) (edge : Node.t edge) : bool =
let from_, to_, cost = edge in
match Hashtbl.find dist from_, Hashtbl.find dist to_ with
...
| _ -> was_updated || falseNotice how once we return a So the parent let relax_edges (edges : Node.t edge list) (dist : tbl) (i : int)
...
else
let is_dist_updated = List.fold_left (relax_edge dist) false edges
in
if is_dist_updated then `Continue dist
else `Stop distThen building the final distance table state is just a matter of initializing the table and then iterating over the nodes: let find_shortest_paths ~(src : Node.t) (edges : Node.t edge list) =
let dist = create_tbl ~src edges in
let num_nodes = Hashtbl.length dist in
let vertices = List.init num_nodes Fun.id in
let final_tbl = List_utils.fold_until
(relax_edges edges)
Fun.id
dist
vertices
in
final_tblWhere let create_tbl ~(src : Node.t) (edges : Node.t edge list) =
let bindings =
edges
|> to_node_list
|> List.map (fun node -> node, (None, None))
|> List.to_seq
in
let tbl = Hashtbl.of_seq bindings in
let () =
Hashtbl.replace tbl src (Some 0, None)
in
tblPredecessors and the Minimum Distance PathYou can think of let bellman_ford
(type node)
(module Node : Baby.OrderedType with type t = node)
~(src : node)
(edges : node edge list)
: [ `No_negative_cycle of (node * int) list
| `Negative_cycle of node edge list
] =
let open Make (Node) in
let tbl = find_shortest_paths ~src edges in
match find_cycle_entry_opt edges tbl with
| None -> `No_negative_cycle (
tbl
|> Hashtbl.to_seq_keys
|> Seq.map (fun node -> node, find_distance node tbl)
|> List.of_seq
)The only change we make to the bindings is picking out the first tuple element let find_distance (node : Node.t) (dist : tbl) : int =
match fst @@ Hashtbl.find dist node with
| None -> Int.max_int
| Some v -> vWe do this because our distance table state encodes a second "predecessor edge" for the second element in the values. The predecessor edge is the edge that connects the previous node in the shortest-distance path to the key-ed node. In other words, it is the edge that caused the last update to the distance state for that particular node. For let relax_edge (dist : tbl) (was_updated : bool) (edge : Node.t edge) : bool =
let from_, to_, cost = edge in
match Hashtbl.find dist from_, Hashtbl.find dist to_ with
| (Some du, _), (None, _) ->
Hashtbl.replace dist to_ (Some (du + cost), Some edge);
...
| (Some du, _), (Some dv, _) when du + cost < dv ->
Hashtbl.replace dist to_ (Some (du + cost), Some edge);
...You can think of the We can lookup the let find_predecessor_edge (node : Node.t) (dist : tbl)
: Node.t edge option =
snd @@ Hashtbl.find dist nodeAnd we can lookup the let find_predecessor (node : Node.t) (tbl : tbl) : Node.t option =
Option.map (fun (from_, _, _) -> from_) (find_predecessor_edge node tbl)
Revisiting our let edges =
[ ('a', '0', 3)
; ('0', 'a', -1)
; ('z', '0', 5)
; ('z', 'a', 5)
]graph LR
na["a"] -->|"3"| n0["0"]
n0 -->|"-1"| na["a"]
nz["z"] -->|"5"| n0["0"]
nz -->|"5"| na["a"]
The shortest path from let module BellmanFord = Bellman_ford.Make (Char) in
let dist = BellmanFord.find_shortest_paths ~src:'z' edges in
Printf.printf "Minimum distance to 'a' = %d\n" (fst @@ Hashtbl.find dist 'a');Minimum distance to 'a' = 4The shortest distance path is let predecessor_edge_of_a = BellmanFord.find_predecessor_edge 'a' dist in
Printf.printf "Predecessor edge is: %s\n" (pp_edge_opt predecessor_edge_of_a);Predecessor edge is: 0 -> a (-1)
let predecessor_edge_of_0 = BellmanFord.find_predecessor_edge '0' dist in
Printf.printf "Predecessor edge is: %s\n" (pp_edge_opt predecessor_edge_of_0);Predecessor edge is: z -> 0 (5)Leads us back to our source node Detecting Negative CyclesFor the purposes of the Blue3 solver, we don't care about the shortest distance paths and only care about the distance values themselves when bellman ford is able to return a meaningful distance table. This is why we filter out the predecessor in the let bellman_ford
...
match find_cycle_entry_opt edges tbl with
| None -> `No_negative_cycle (
tbl
|> Hashtbl.to_seq
|> Seq.map (fun node -> node, find_distance node tbl)The predecessor becomes useful when let bellman_ford
...
match find_cycle_entry_opt edges tbl with
| None -> ...
| Some entry -> (* we need predecessor to handle this case *)
let find_cycle_entry_opt (edges : Node.t edge list) (dist : tbl)
: Node.t option =
...
let relaxed_predecessor = find_relaxed_node_opt edges dist inIf let find_relaxed_node_opt (edges : Node.t edge list) (dist : tbl) : Node.t option =
List.find_map (fun ((_, to_, _) as edge) ->
if relax_edge dist false edge then
Some to_
else None)
edgesThe basic idea is that the minimum distance paths take at most This returned node by Consider the graph where we have the negative cycle of graph LR
c["c"] -->|"0"| d["d"]
s["s"] -->|"0"| a["a"]
a["a"] -->|"1"| b["b"]
b["b"] -->|"-4"| c["c"]
c["c"] -->|"1"| a["a"]
If we just used the node from let edges =
[ ('c', 'd', 0) (* outgoing edge from cycle to non-cycle node *)
; ('s', 'a', 0)
; ('a', 'b', 1)
; ('b', 'c', -4)
; ('c', 'a', 1)
]
in
let cycle_entry = BellmanFord.find_relaxed_node edges (fst dist) in
Printf.printf "First relaxed node found: %c\n" cycle_entry;
let cycle_from_entry = BellmanFord.collect_cycle cycle_entry dist in
List.iter (fun edge ->
Printf.printf "- %s\n" (pp_edge edge))
cycle_from_entryFirst relaxed node found: d
- b -> c (-4)
- c -> a (1)
- a -> b (1)
- b -> c (-4)
- c -> d (0)To find a node that is actually in the cycle, we have to back track from let find_cycle_entry_opt (edges : Node.t edge list) (tbl, num_nodes : t)
: Node.t option =
let relaxed_predecessor = find_relaxed_node_opt edges tbl in
match relaxed_predecessor with
| None -> None
| Some entry ->
let rec move_back node n =
if n = 0 then node
else if n < num_nodes && node = entry then node
else
match find_predecessor node tbl with
| None -> node
| Some from_ -> move_back from_ (n - 1)
in
Some (move_back entry num_nodes)Then with this entry node found, we can terminate the algorithm by building the negative cycle. We build it by backtracking one more time along the predecessor edges starting from our entry node: let collect_cycle (start : Node.t) (tbl, num_nodes : t) : Node.t edge list =
let rec loop curr n acc =
if n = 0 then
acc
else
match find_predecessor_edge curr tbl with
| None -> acc
| Some ((from_, _, _) as pred_edge) ->
let acc = pred_edge :: acc in
if Node.compare from_ start = 0 then acc
else loop from_ (n - 1) acc
in
loop start num_nodes []And that's all Bellman Ford needs to return the negative cycle edges: let bellman_ford
(type node)
(module Node : Baby.OrderedType with type t = node)
~(src : node)
(edges : node edge list)
: [ `No_negative_cycle of (node * int) list
| `Negative_cycle of node edge list
] =
let open Make (Node) in
let tbl = find_shortest_paths ~src edges in
match find_cycle_entry_opt edges tbl with
| None -> ...
| Some entry -> `Negative_cycle (collect_cycle entry tbl)let edges =
[ ('s', 'a', 2)
; ('a', 'b', 1)
; ('b', 'c', -4)
; ('c', 'a', 1)
; ('c', 'd', 3)
]
in
let dist = BellmanFord.find_shortest_paths ~src:'s' edges in
let cycle_entry = BellmanFord.find_cycle_entry edges dist in
let cycle_from_entry = BellmanFord.collect_cycle cycle_entry dist in
Printf.printf "Negative cycle found:\n";
List.iter (fun edge ->
Printf.printf "- %s\n" (pp_edge edge))
cycle_from_entry;Negative cycle found:
- b -> c (-4)
- c -> a (1)
- a -> b (1) |
Features added
solve.mlinteger.mlboolean.mlOverview
I want to merge in my toy SMT solver ("Blue3") into the concolic evaluator so it can attempt to fast-solve certain formulas. The IDL solver and Boolean text parser are more-or-less ported over 1-to-1. The primary change from the port is writing out a basic DPLL (T) solver loop.
Benchmarks
Here are the benchmark results you can find by inputting
analysis.sqlinto an SQLite databaseafter running the benchmark script:
What were the average runtimes from both solvers?
Rows with the MIN times from both solvers
Rows with the MAX times from both solvers
How much faster were the fast cases on average?
How much slower were the slow cases on average?
What was the max time difference blue3 beat z3 by?
What was the max time difference z3 beat blue3 by?