Dijkstra algorithm is a shortest path algorithm that is used to find the shortest path between two nodes in a graph. It was conceived by Edsger W. Dijkstra in 1956 and published three years later. The algorithm is used in various applications such as transportation networks, computer networks, and social networks.
The Dijkstra algorithm is implemented in OCaml as follows:
let list_vertices graph =
List.fold_left (fun acc ((a, b), _) ->
let acc = if List.mem b acc then acc else b::acc in
let acc = if List.mem a acc then acc else a::acc in
acc
) [] graph
let neighbors v =
List.fold_left (fun acc ((a, b), d) ->
if a = v then (b, d)::acc else acc
) []
let remove_from v lst =
let rec aux acc = function [] -> failwith "remove_from"
| x::xs -> if x = v then List.rev_append acc xs else aux (x::acc) xs
in aux [] lst
let with_smallest_distance q dist =
match q with
| [] -> assert false
| x::xs ->
let rec aux distance v = function
| x::xs ->
let d = Hashtbl.find dist x in
if d < distance
then aux d x xs
else aux distance v xs
| [] -> (v, distance)
in
aux (Hashtbl.find dist x) x xs
let dijkstra max_val zero add graph source target =
let vertices = list_vertices graph in
let dist_between u v =
try List.assoc (u, v) graph
with _ -> zero
in
let dist = Hashtbl.create 1 in
let previous = Hashtbl.create 1 in
List.iter (fun v -> (* initializations *)
Hashtbl.add dist v max_val (* unknown distance function from source to v *)
) vertices;
Hashtbl.replace dist source zero; (* distance from source to source *)
let rec loop = function [] -> ()
| q ->
let u, dist_u =
with_smallest_distance q dist in (* vertex in q with smallest distance in dist *)
if dist_u = max_val then
failwith "vertices inaccessible"; (* all remaining vertices are inaccessible from source *)
if u = target then () else begin
let q = remove_from u q in
List.iter (fun (v, d) ->
if List.mem v q then begin
let alt = add dist_u (dist_between u v) in
let dist_v = Hashtbl.find dist v in
if alt < dist_v then begin (* relax (u,v,a) *)
Hashtbl.replace dist v alt;
Hashtbl.replace previous v u; (* previous node in optimal path from source *)
end
end
) (neighbors u graph);
loop q
end
in
loop vertices;
let s = ref [] in
let u = ref target in
while Hashtbl.mem previous !u do
s := !u :: !s;
u := Hashtbl.find previous !u
done;
(source :: !s)
The function dijkstra
takes in a graph represented as a list of edges and their weights, a maximum value, a zero value, an addition function, a source node, and a target node. It returns a list of nodes representing the shortest path from the source to the target.
Here is how to call the function.
let () =
let graph =
[ ("a", "b"), 7;
("a", "c"), 9;
("a", "f"), 14;
("b", "c"), 10;
("b", "d"), 15;
("c", "d"), 11;
("c", "f"), 2;
("d", "e"), 6;
("e", "f"), 9; ]
in
let p = dijkstra max_int 0 (+) graph "a" "e" in
print_endline (String.concat " -> " p)
The function list_vertices
takes in the graph and returns a list of all the vertices in the graph. It does this by iterating over each edge in the graph and adding its endpoints to the list of vertices if they are not already in it.
The function neighbors
takes in a vertex v
and returns a list of its neighbors and their distances. It does this by iterating over each edge in the graph and adding the neighbor of v
and its distance to the list if v
is the start point of the edge.
The function remove_from
takes in a vertex v
and a list lst
and returns a new list with v
removed from it. It does this by recursively iterating over the list and adding each element to a new list except for v
.
The function with_smallest_distance
takes in a list of vertices q
and a hash table of distances dist
and returns the vertex in q
with the smallest distance in dist
. It does this by recursively iterating over the list and comparing the distance of each vertex to the current smallest distance.
The function dijkstra
initializes the hash table of distances dist
and the hash table of previous vertices previous
. It then iterates over each vertex in the graph and adds it to dist
with a distance of max_val
except for the source node, which is added with a distance of zero
. It then enters a loop where it selects the vertex in q
with the smallest distance in dist
using with_smallest_distance
. If the distance is max_val
, it means all remaining vertices are inaccessible from the source, so it throws an error. If the selected vertex is the target node, it exits the loop. Otherwise, it removes the selected vertex from q
and iterates over its neighbors. For each neighbor, it computes a new distance alt
from the source to the neighbor through the selected vertex and updates dist
and previous
if alt
is smaller than the current distance in dist
. It then continues the loop with the updated q
.
The function dijkstra
ends by constructing the shortest path from the source to the target using the hash table of previous vertices previous
. It starts at the target and iteratively adds the previous vertex to the path until it reaches the source.
The time complexity of Dijkstra’s algorithm is O((E+V)logV), where E is the number of edges and V is the number of vertices in the graph. This is because the algorithm iterates over each vertex once and each edge once, and it uses a priority queue to select the vertex with the smallest distance in each iteration, which takes logV time. The space complexity is O(V+E) because it stores the hash tables of distances and previous vertices, which have a size proportional to the number of vertices and edges in the graph.
In terms of the input parameters, the time complexity of list_vertices
is O(E), where E is the number of edges in the graph, because it iterates over each edge once. The time complexity of neighbors
is also O(E), because it iterates over each edge once. The time complexity of remove_from
is O(n), where n is the length of the input list, because it iterates over each element once. The time complexity of with_smallest_distance
is O(V), where V is the number of vertices in the input list, because it iterates over each vertex once. Therefore, the overall time complexity of the Dijkstra algorithm is dominated by the time complexity of the main loop, which is O((E+V)logV).
In terms of the space complexity, the hash tables used in the algorithm have a size proportional to the number of vertices in the graph, which is O(V). The priority queue used in with_smallest_distance
also has a size proportional to the number of vertices, which is O(V). Therefore, the overall space complexity of the algorithm is O(V+E).
Overall, the Dijkstra algorithm is an efficient algorithm for finding the shortest path between two nodes in a graph. Its time complexity is proportional to the number of edges and vertices in the graph, and its space complexity is proportional to the number of vertices.
]]>The Caesar cipher is one of the simplest and most widely known encryption techniques. It is a type of substitution cipher in which each letter in the plaintext is shifted a certain number of places down the alphabet. For example, with a shift of 1, A would be replaced by B, B would become C, and so on. The method is apparently named after Julius Caesar, who used it to communicate with his officials.
The Caesar cipher is implemented in OCaml as follows:
let islower c =
c >= 'a' && c <= 'z'
let isupper c =
c >= 'A' && c <= 'Z'
let rot x str =
let upchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
and lowchars = "abcdefghijklmnopqrstuvwxyz" in
let rec decal x =
if x < 0 then decal (x + 26) else x
in
let x = (decal x) mod 26 in
let decal_up = x - (int_of_char 'A')
and decal_low = x - (int_of_char 'a') in
String.map (fun c ->
if islower c then
let j = ((int_of_char c) + decal_low) mod 26 in
lowchars.[j]
else if isupper c then
let j = ((int_of_char c) + decal_up) mod 26 in
upchars.[j]
else
c
) str
The rot
function takes an integer x
and a string str
as arguments. The integer x
represents the number of positions to shift each letter in the string str
. The function returns a new string that is the result of applying the Caesar cipher with the given shift.
Here is how to use the function.
let () =
let key = 3 in
let orig = "The five boxing wizards jump quickly" in
let enciphered = rot key orig in
print_endline enciphered;
let deciphered = rot (- key) enciphered in
print_endline deciphered;
Printf.printf "equal: %b\n" (orig = deciphered)
;;
islower
function checks if a given character is a lowercase letter.isupper
function checks if a given character is an uppercase letter.rot
function initializes two strings: upchars
containing all uppercase letters and lowchars
containing all lowercase letters.decal
function takes an integer x
and returns the result of decrementing x
by 26 until it is non-negative.x
variable is assigned the result of applying decal
to x
and then taking the modulus of 26.decal_up
variable is assigned the value of x
minus the integer value of the character ‘A’.decal_low
variable is assigned the value of x
minus the integer value of the character ‘a’.String.map
function applies a function to each character in the string str
.c
in the string str
, the function checks if it is a lowercase letter using the islower
function.c
is a lowercase letter, the function calculates a new index j
by adding the integer value of c
to decal_low
and taking the modulus of 26. The new character is then obtained by taking the j
-th character of the lowchars
string.c
is an uppercase letter, the function calculates a new index j
by adding the integer value of c
to decal_up
and taking the modulus of 26. The new character is then obtained by taking the j
-th character of the upchars
string.c
is not a letter, the function returns c
unchanged.The time complexity of the Caesar cipher algorithm is O(n), where n
is the length of the input string str
. This is because the String.map
function applies a function to each character in the string str
, and this operation takes O(1) time per character. Therefore, the overall time complexity is O(n).
The space complexity of the algorithm is also O(n), since the resulting string has the same length as the input string.
In terms of security, the Caesar cipher is very weak and easy to break, since there are only 26 possible shifts and an attacker can easily try all of them. Therefore, it is not suitable for use in modern cryptography, but it can still be used for simple purposes such as obfuscation or educational purposes.
]]>The Best Shuffle algorithm is used to shuffle a string such that no two consecutive characters remain the same. It is a simple algorithm that can be used in various applications such as cryptography, data encryption, and data compression.
The implementation of the Best Shuffle algorithm is provided below in OCaml. The algorithm takes a string as input and returns the shuffled string.
let best_shuffle s =
let len = String.length s in
let r = String.copy s in
for i = 0 to pred len do
for j = 0 to pred len do
if i <> j && s.[i] <> r.[j] && s.[j] <> r.[i] then
begin
let tmp = r.[i] in
r.[i] <- r.[j];
r.[j] <- tmp;
end
done;
done;
(r)
Here is an example.
let count_same s1 s2 =
let len1 = String.length s1
and len2 = String.length s2 in
let n = ref 0 in
for i = 0 to pred (min len1 len2) do
if s1.[i] = s2.[i] then incr n
done;
!n
let () =
let test s =
let s2 = best_shuffle s in
Printf.printf " '%s', '%s' -> %d\n" s s2 (count_same s s2);
in
test "tree";
test "abracadabra";
test "seesaw";
test "elk";
test "grrrrrr";
test "up";
test "a";
;;
len
.r
.The time complexity of the Best Shuffle algorithm is O(n^2), where n is the length of the input string. This is because the algorithm uses a nested loop to iterate over each pair of characters in the input string. The space complexity of the algorithm is O(n), where n is the length of the input string. This is because the algorithm creates a copy of the input string to store the shuffled string. Overall, the Best Shuffle algorithm is a simple and efficient algorithm for shuffling strings.
]]>Balanced brackets is a problem that requires checking if a given string of brackets is balanced or not. A balanced string of brackets is one that has the same number of opening and closing brackets, and the brackets are properly nested. This problem has many applications, such as in compilers, text editors, and syntax checkers.
let generate_brackets n =
let rec aux i acc =
if i <= 0 then acc else
aux (pred i) ('['::']'::acc)
in
let brk = aux n [] in
List.sort (fun _ _ -> (Random.int 3) - 1) brk
let is_balanced brk =
let rec aux = function
| [], 0 -> true
| '['::brk, level -> aux (brk, succ level)
| ']'::brk, 0 -> false
| ']'::brk, level -> aux (brk, pred level)
| _ -> assert false
in
aux (brk, 0)
let () =
let n = int_of_string Sys.argv.(1) in
Random.self_init();
let brk = generate_brackets n in
List.iter print_char brk;
Printf.printf " %B\n" (is_balanced brk);
;;
The algorithm implemented here generates a random string of n brackets, either [
or ]
, and then checks if it is balanced. The generate_brackets
function generates a list of n brackets, alternating between [
and ]
, and then shuffles the list randomly. The is_balanced
function checks if the brackets are balanced by using a stack to keep track of the number of opening brackets seen so far. If a closing bracket is encountered while the stack is empty, or if the closing bracket does not match the most recent opening bracket, the string is not balanced.
generate_brackets
function takes an integer n
as input and returns a list of n
brackets.aux
that takes an integer i
and a list acc
as input, where i
is the number of brackets left to generate and acc
is the accumulator list that holds the generated brackets so far.i
is less than or equal to 0, the function returns the accumulator list.aux
recursively with i-1
and ['['; ']'] @ acc
, which adds an opening and closing bracket to the accumulator list.generate_brackets
function then shuffles the list randomly using List.sort
and a comparison function that randomly returns -1, 0, or 1.is_balanced
function takes a list of brackets brk
as input and returns a boolean indicating if the brackets are balanced or not.aux
that takes a tuple (brk, level)
as input, where brk
is the remaining list of brackets and level
is the number of opening brackets seen so far.brk
is empty and level
is 0, the function returns true, indicating that the brackets are balanced.brk
is [
, the function calls aux
recursively with the tail of brk
and level+1
.brk
is ]
, the function checks if level
is 0, indicating that there is no opening bracket to match the closing bracket. If level
is not 0, the function calls aux
recursively with the tail of brk
and level-1
.brk
is neither [
nor ]
, the function raises an assertion error, since this should never happen.is_balanced
function calls aux
with (brk, 0)
as the initial tuple, since there are no opening brackets seen so far.n
from the command line arguments, generates a random list of n
brackets using generate_brackets
, prints the brackets to the console, and then checks if they are balanced using is_balanced
.The time complexity of generating the list of brackets is O(n), since it calls the aux
function recursively n times, and each call takes constant time to append two brackets to the accumulator list. The time complexity of shuffling the list is O(n log n), since it uses List.sort
with a comparison function that takes constant time. The time complexity of checking if the brackets are balanced is O(n), since it scans the list once and uses a stack to keep track of the opening brackets seen so far. The space complexity of generating the list of brackets is O(n), since it creates a list of size n. The space complexity of checking if the brackets are balanced is also O(n), since it uses a stack that can hold up to n opening brackets.
In summary, the overall time complexity of the algorithm is O(n log n), due to the sorting step, and the overall space complexity is O(n), due to the list and stack used.
]]>The A* algorithm is a pathfinding algorithm used in artificial intelligence and robotics to find the shortest path between two points. It is widely used in video games, GPS systems, and other applications that require finding optimal paths. The algorithm uses a heuristic function to estimate the distance between the current node and the goal node, and combines it with the cost of the path from the start node to the current node to determine the best next step.
The A* algorithm is implemented in OCaml using a priority queue to store the open set of nodes to be explored. The algorithm also uses two sets, closedSet and openSet, to keep track of visited and unvisited nodes, respectively. The function find_path
takes the starting and goal positions, as well as a board representing the environment in which the path is to be found. The board is represented as a two-dimensional array of integers, where 0 represents an obstacle and any positive integer represents a clear path.
module IntPairs =
struct
type t = int * int
let compare (x0,y0) (x1,y1) =
match Stdlib.compare x0 x1 with
| 0 -> Stdlib.compare y0 y1
| c -> c
end
module PairsMap = Map.Make(IntPairs)
module PairsSet = Set.Make(IntPairs)
let find_path start goal board =
let max_y = Array.length board in
let max_x = Array.length board.(0) in
let get_neighbors (x, y) =
let moves = [(0, 1); (0, -1); (1, 0); (-1, 0);
(1, 1); (1, -1); (-1, 1); (-1, -1)] in
let ms = List.map (fun (_x, _y) -> x+_x, y+_y) moves in
let ms = List.filter (fun (x, y) ->
x >= 0 && x < max_x && y >= 0 && y < max_y
&& board.(y).(x) <> 0
) ms in
(ms)
in
let h (x0, y0) (x1, y1) =
abs (x0 - x1) + abs (y0 - y1)
in
let openSet = PairsSet.add start PairsSet.empty in
let closedSet = PairsSet.empty in
let fScore = PairsMap.add start (h goal start) PairsMap.empty in
let gScore = PairsMap.add start 0 PairsMap.empty in
let cameFrom = PairsMap.empty in
let reconstruct_path cameFrom current =
let rec aux acc current =
let from = PairsMap.find current cameFrom in
if from = start then (from::acc)
else aux (from::acc) from
in
aux [current] current
in
let d current neighbor =
let x, y = neighbor in
board.(y).(x)
in
let g gScore cell =
match PairsMap.find_opt cell gScore with
| Some v -> v | None -> max_int
in
let rec _find_path (openSet, closedSet, fScore, gScore, cameFrom) =
if PairsSet.is_empty openSet then None else
let current =
PairsSet.fold (fun p1 p2 ->
if p2 = (-1, -1) then p1 else
let s1 = PairsMap.find p1 fScore
and s2 = PairsMap.find p2 fScore in
if s1 < s2 then p1 else p2
) openSet (-1, -1)
in
if current = goal then Some (reconstruct_path cameFrom current) else
let openSet = PairsSet.remove current openSet in
let closedSet = PairsSet.add current closedSet in
let neighbors = get_neighbors current in
neighbors |>
List.fold_left
(fun ((openSet, closedSet, fScore, gScore, cameFrom) as v) neighbor ->
if PairsSet.mem neighbor closedSet then (v) else
let tentative_gScore = (g gScore current) + (d current neighbor) in
if tentative_gScore < (g gScore neighbor) then
let cameFrom = PairsMap.add neighbor current cameFrom in
let gScore = PairsMap.add neighbor tentative_gScore gScore in
let f = (g gScore neighbor) + (h neighbor goal) in
let fScore = PairsMap.add neighbor f fScore in
let openSet =
if not (PairsSet.mem neighbor openSet)
then PairsSet.add neighbor openSet else openSet
in
(openSet, closedSet, fScore, gScore, cameFrom)
else (v)
) (openSet, closedSet, fScore, gScore, cameFrom)
|> _find_path
in
_find_path (openSet, closedSet, fScore, gScore, cameFrom)
Here is an example.
let () =
let board = [|
[| 1; 1; 1; 1; 1; 1; 1; 1; |];
[| 1; 1; 1; 1; 1; 1; 1; 1; |];
[| 1; 1; 1; 0; 0; 0; 1; 1; |];
[| 1; 1; 1; 1; 1; 0; 1; 1; |];
[| 1; 1; 0; 1; 1; 0; 1; 1; |];
[| 1; 1; 0; 1; 1; 0; 1; 1; |];
[| 1; 1; 0; 0; 0; 0; 1; 1; |];
[| 1; 1; 1; 1; 1; 1; 1; 1; |];
|] in
let start = (0, 0) in
let goal = (7, 7) in
let dim_x = Array.length board.(0) in
let dim_y = Array.length board in
let r = find_path start goal board in
match r with
| None -> failwith "path not found"
| Some p ->
List.iter (fun (x, y) -> Printf.printf " (%d, %d)\n" x y) p;
print_newline ();
let _board =
Array.init dim_y (fun y ->
Array.init dim_x (fun x ->
if board.(y).(x) = 0 then '#' else '.'))
in
List.iter (fun (x, y) -> _board.(y).(x) <- '*') p;
Array.iter (fun line ->
Array.iter (fun c ->
Printf.printf " %c" c;
) line;
print_newline ()
) _board;
print_newline ()
Array.length
function.get_neighbors
function takes a position (x,y) and returns a list of neighboring positions that are not obstacles.h
function takes two positions and returns the Manhattan distance between them.fScore
and gScore
maps are initialized with the starting position, where fScore
is the sum of gScore
and the heuristic function.cameFrom
map is initialized as empty.reconstruct_path
function takes the cameFrom
map and the current position and returns the path from the starting position to the current position.d
function takes two positions and returns the cost of moving from the first position to the second.g
function takes the gScore
map and a position and returns its gScore
value._find_path
function takes the current state of the search and performs the following steps:
fScore
value from the openSet.gScore
value for the node.gScore
value is better than the current value, update the gScore
, fScore
, and cameFrom
maps._find_path
with the updated state._find_path
with the initial state.The time complexity of the A* algorithm depends on the heuristic function used and the size of the board. In the worst case, where the heuristic function overestimates the true distance, the algorithm can explore all nodes in the board, resulting in a time complexity of O(b^d), where b is the branching factor (number of neighbors per node) and d is the depth of the goal node. However, in practice, the algorithm often explores fewer nodes than this worst-case bound.
The space complexity of the algorithm is also dependent on the size of the board, as it stores the openSet, closedSet, fScore
, gScore
, and cameFrom
maps. However, the space complexity can be reduced by using a more memory-efficient data structure for the
Additive prime is a prime number whose digits add up to another prime number. For example, 23 is an additive prime because 2 + 3 = 5, and both 23 and 5 are prime numbers. This algorithm checks whether a given number is an additive prime.
The algorithm is implemented in OCaml. The function digit_sum
calculates the sum of digits of a given number. The function is_prime
checks whether a given number is prime. The function is_additive_prime
checks whether a given number is an additive prime by checking if the number and its digit sum are both prime.
let rec digit_sum n =
if n < 10 then n else n mod 10 + digit_sum (n / 10)
let is_prime n =
let rec test x =
let q = n / x in x > q || x * q <> n && n mod (x + 2) <> 0 && test (x + 6)
in if n < 5 then n lor 1 = 3 else n land 1 <> 0 && n mod 3 <> 0 && test 5
let is_additive_prime n =
is_prime n && is_prime (digit_sum n)
Here is an example.
let () =
Seq.ints 0 |> Seq.take_while ((>) 500) |> Seq.filter is_additive_prime
|> Seq.iter (Printf.printf " %u") |> print_newline
digit_sum
takes a positive integer n
as input.n
is less than 10, then n
is returned as the sum of digits of a single-digit number is the number itself.n
is greater than or equal to 10, then the last digit of n
is added to the sum of digits of the remaining digits of n
by recursively calling the digit_sum
function.is_prime
takes a positive integer n
as input.n
is less than 5, then n
is prime if and only if it is equal to 2 or 3.n
is greater than or equal to 5, then the function test
is called with an initial value of 5.test
takes a positive integer x
as input and checks if n
is divisible by x
or x + 2
, or if n
is divisible by any number of the form x + 6k
where k
is a non-negative integer less than or equal to (n / x - x) / 6
.x
is greater than (n / x - x) / 6
, then n
is prime.is_additive_prime
takes a positive integer n
as input.n
is not prime, then is_additive_prime
returns false
.n
is prime, then the function digit_sum
is called with n
as input.n
is not prime, then is_additive_prime
returns false
.n
and its digit sum are prime, then is_additive_prime
returns true
.The time complexity of the digit_sum
function is O(log n) as the number of digits in n
is proportional to log n. The time complexity of the is_prime
function is O(sqrt n) as it checks divisibility up to the square root of n
. The worst-case time complexity of the is_additive_prime
function is O(sqrt n log n) as it checks the primality of n
and its digit sum, which each have a worst-case time complexity of O(sqrt n). The space complexity of all
Best-first search is a graph search algorithm that aims to find the optimal path between two nodes in a graph. It is commonly used in artificial intelligence and computer science, particularly in the field of pathfinding. The algorithm works by exploring the most promising nodes first, based on a heuristic function that estimates the distance to the goal node. Best-first search can be used in a variety of applications, such as route planning, game AI, and logistics.
Here is an implementation of Best-first search in OCaml:
type 'a node = {
state: 'a;
parent: 'a node option;
cost: float;
heuristic: float;
}
let best_first_search initial_state goal_state successors heuristic =
let is_goal n = n.state = goal_state in
let rec search frontier visited =
match frontier with
| [] -> None
| n :: _ when is_goal n -> Some n
| n :: ns ->
let successors = successors n.state in
let add_node frontier node =
if List.exists (fun n -> n.state = node.state) visited then frontier
else List.merge (fun n1 n2 -> compare n1.cost n2.cost)
frontier [node] in
let nodes = List.map (fun (state, cost) ->
let heuristic = heuristic state in
{state; parent = Some n; cost; heuristic}) successors in
let frontier' = List.fold_left add_node ns nodes in
let visited' = n :: visited in
search frontier' visited'
in
let initial_node = {state = initial_state; parent = None; cost = 0.; heuristic = heuristic initial_state} in
search [initial_node] []
Here, type 'a node
represents a node in the search tree. Each node has a state, a parent node, a cost (i.e., the cumulative cost from the initial state), and a heuristic value (i.e., the estimated distance to the goal state). The best_first_search
function takes as input the initial state, the goal state, a function that generates the successors of a given state, and a heuristic function that estimates the distance to the goal state. The function returns an optional node that represents the goal state, or None
if no path was found.
Here’s an example of using Best-first search to find the shortest path between two cities in a graph:
let graph = [
("A", [("B", 7.); ("C", 3.)]);
("B", [("A", 7.); ("C", 1.); ("D", 2.)]);
("C", [("A", 3.); ("B", 1.); ("D", 2.)]);
("D", [("B", 2.); ("C", 2.); ("E", 4.)]);
("E", [("D", 4.)])
]
let successors state =
let rec find_node state = function
| [] -> failwith "Node not found"
| (s, _) :: _ when s = state -> []
| _ :: rest -> find_node state rest
in
let (_, edges) = List.find (fun (s, _) -> s = state) graph in
List.map (fun (s, c) -> (s, c)) edges @ find_node state graph
let heuristic state =
match state with
| "A" -> 6.
| "B" -> 2.
| "C" -> 4.
| "D" -> 2.
| "E" -> 0.
| _ -> failwith "Invalid state"
let path = best_first_search "A" "E" successors heuristic
let print_path path =
let rec print_node node =
match node.parent with
| None -> print_endline node.state
| Some parent ->
print_node parent;
print_endline node.state
in
match path with
| None -> print_endline "No path found"
| Some node -> print_node node
let () = print_path path
This code defines a graph as a list of nodes, where each node is a tuple of a state and a list of edges to other nodes, each with a cost. The successors
function generates the successors of a given state by looking up the corresponding node in the graph and returning its edges. The heuristic
function estimates the distance to the goal state based on a predefined heuristic value for each state.
The path
variable contains the result of running Best-first search on the graph, starting from state “A” and ending at state “E”. Finally, the print_path
function prints out the path from the initial state to the goal state.
When we run this code, we should see the following output:
A
C
D
E
This indicates that the shortest path from “A” to “E” is “A” -> “C” -> “D” -> “E”, with a total cost of 9.
None
(i.e., no path was found).The key idea behind Best-first search is to explore the most promising nodes first, based on a heuristic function that estimates the distance to the goal node. This allows the algorithm to quickly converge to the optimal path, while avoiding exploring unpromising paths.
The time complexity of Best-first search depends on the quality of the heuristic function. In the worst case, where the heuristic function is not informative (i.e., it always returns 0), the algorithm degenerates to a breadth-first search, with a time complexity of O(b^d), where b is the branching factor of the graph and d is the depth of the goal node. In the
]]>The inverse of a matrix is a matrix that, when multiplied by the original matrix, results in the identity matrix. The inverse of a matrix is useful in solving systems of linear equations, computing determinants, and in other areas of mathematics. The algorithm we will be discussing is used to find the inverse of a square matrix.
let inv varr =
let _k = M.kind varr in
let _add = Owl_base_dense_common._add_elt _k in
let _mul = Owl_base_dense_common._mul_elt _k in
let _div = Owl_base_dense_common._div_elt _k in
let _neg = Owl_base_dense_common._neg_elt _k in
let _zero = Owl_const.zero _k in
let _one = Owl_const.one _k in
let dims = M.shape varr in
let _ = _check_is_matrix dims in
let n = Array.unsafe_get dims 0 in
if Array.unsafe_get dims 1 != n
then failwith "no inverse - the matrix is not square"
else (
let pivot_row = Array.make n _zero in
let result_varr = M.copy varr in
for p = 0 to n - 1 do
let pivot_elem = M.get result_varr [| p; p |] in
if M.get result_varr [| p; p |] = _zero
then failwith "the matrix does not have an inverse";
(* update elements of the pivot row, save old vals *)
for j = 0 to n - 1 do
pivot_row.(j) <- M.get result_varr [| p; j |];
if j != p then M.set result_varr [| p; j |] (_div pivot_row.(j) pivot_elem)
done;
(* update elements of the pivot col *)
for i = 0 to n - 1 do
if i != p
then
M.set
result_varr
[| i; p |]
(_div (M.get result_varr [| i; p |]) (_neg pivot_elem))
done;
(* update the rest of the matrix *)
for i = 0 to n - 1 do
let pivot_col_elem = M.get result_varr [| i; p |] in
for j = 0 to n - 1 do
if i != p && j != p
then (
let pivot_row_elem = pivot_row.(j) in
(* use old value *)
let old_val = M.get result_varr [| i; j |] in
let new_val = _add old_val (_mul pivot_row_elem pivot_col_elem) in
M.set result_varr [| i; j |] new_val)
done
done;
(* update the pivot element *)
M.set result_varr [| p; p |] (_div _one pivot_elem)
done;
result_varr)
The algorithm is implemented in the OCaml programming language. It takes as input a matrix represented as an Owl ndarray and returns the inverse of the matrix as an ndarray. Here is an example of how to use the inv
function:
let m = Owl.Mat.of_array [| [| 1.; 2.; 3. |]; [| 4.; 5.; 6. |]; [| 7.; 8.; 10. |] |] in
let m_inv = inv m in
Owl.Mat.print m_inv
This will print the inverse of the matrix m
.
The algorithm works by performing a series of operations on the input matrix until it is transformed into the identity matrix. These operations are performed on both the input matrix and an identity matrix, which is used to keep track of the transformations. Here are the steps of the algorithm:
The time complexity of the algorithm is O(n^3), where n is the size of the input matrix. This is because the algorithm performs O(n) operations for each of the n diagonal elements, resulting in a total of O(n^3) operations. The space complexity of the algorithm is also O(n^3), since it creates a copy of the input matrix and a pivot row array of size n. Therefore, the algorithm is not suitable for very large matrices, as it may take a long time and use a lot of memory to compute the inverse.
]]>The tridiagonal matrix algorithm is a numerical method used to solve a system of linear equations where the matrix is tridiagonal. A tridiagonal matrix is a matrix where all the elements are zero except for those on the main diagonal, the diagonal above it, and the diagonal below it. The algorithm is used in various fields such as physics, engineering, and finance.
(* Solver for tridiagonal matrix
* Input: a[n], b[n], c[n], which together consist the tridiagonal matrix A, and the right side vector r[n]. Return: x[n].
*)
let tridiag_solve_vec a b c r =
let n = Array.length a in
let n1 = Array.length b in
let n2 = Array.length c in
assert (n = n1 && n = n2);
if b.(0) = 0.
then raise (Invalid_argument "tridiag_solve_vec: 0 at the beginning of diagonal vector");
let bet = ref b.(0) in
let gam = Array.make n 0. in
let x = Array.make n 0. in
x.(0) <- r.(0) /. !bet;
for j = 1 to n - 1 do
gam.(j) <- c.(j - 1) /. !bet;
bet := b.(j) -. (a.(j) *. gam.(j));
if !bet = 0. then raise (Invalid_argument "tridiag_solve_vec: algorithm fails");
x.(j) <- (r.(j) -. (a.(j) *. x.(j - 1))) /. !bet
done;
for j = n - 2 downto 0 do
x.(j) <- x.(j) -. (gam.(j + 1) *. x.(j + 1))
done;
x
The implementation of the tridiagonal matrix algorithm is provided in OCaml. The function tridiag_solve_vec
takes four input arrays a
, b
, c
, and r
, where a
, b
, and c
are the arrays representing the tridiagonal matrix and r
is the right-hand side vector. The function returns the solution vector x
.
The tridiagonal matrix algorithm works by eliminating the coefficients below and above the main diagonal. The algorithm can be divided into three steps:
Decomposition: In this step, the algorithm decomposes the matrix into two matrices, L and U, where L is a lower triangular matrix and U is an upper triangular matrix. The decomposition is done in such a way that the product of L and U is equal to the original matrix.
Forward substitution: In this step, the algorithm solves the equation Ly = r, where L is the lower triangular matrix obtained in the previous step, y is an intermediate vector, and r is the right-hand side vector. This step is called forward substitution because it starts from the first equation and solves for y in terms of the previous y values.
Backward substitution: In this step, the algorithm solves the equation Ux = y, where U is the upper triangular matrix obtained in the first step, x is the solution vector, and y is the intermediate vector obtained in the previous step. This step is called backward substitution because it starts from the last equation and solves for x in terms of the previous x values.
The implementation of the tridiagonal matrix algorithm provided in OCaml is based on the Thomas algorithm, which is a simplified version of the tridiagonal matrix algorithm. The algorithm starts by checking that the diagonal element of the matrix is not zero. If it is zero, the algorithm raises an exception.
The algorithm then proceeds to calculate the intermediate values gam
and bet
, which are used in the forward substitution step. The intermediate value bet
is updated at each iteration, and if it becomes zero, the algorithm raises an exception. The solution vector x
is also updated at each iteration.
Finally, the algorithm performs the backward substitution step to obtain the final solution vector x
.
The time complexity of the tridiagonal matrix algorithm is O(n), where n is the size of the matrix. The algorithm performs two loops, one for the forward substitution step and one for the backward substitution step, each of which takes O(n) time. The intermediate calculations take constant time, so the overall time complexity is O(n).
The space complexity of the algorithm is also O(n), as it requires arrays of size n to store the intermediate values and the solution vector.
]]>LU decomposition is a numerical method for solving systems of linear equations. It decomposes a square matrix into two triangular matrices, one lower triangular matrix (L) and one upper triangular matrix (U). This decomposition can be used to solve linear equations in the form Ax = b, where A is the matrix, x is the vector of unknowns, and b is the vector of constants.
The following implementation of LU decomposition is written in OCaml. It takes a matrix a
as input and returns the L and U matrices, as well as a row permutation vector. The _lu_base
function performs the decomposition and returns the L and U matrices and the permutation vector. The lu
function calls _lu_base
and then formats the output into the L and U matrices. The _lu_solve_vec
function uses the output of _lu_base
to solve a system of linear equations.
(* LU decomposition.
* Input matrix: a[n][n]; return L/U in one matrix, and the row permutation vector.
* Test: https://github.com/scipy/scipy/blob/master/scipy/linalg/tests/test_decomp.py
*)
let _lu_base a =
let k = M.kind a in
let _abs = Owl_base_dense_common._abs_elt k in
let _mul = Owl_base_dense_common._mul_elt k in
let _div = Owl_base_dense_common._div_elt k in
let _sub = Owl_base_dense_common._sub_elt k in
let _flt = Owl_base_dense_common._float_typ_elt k in
let _zero = Owl_const.zero k in
let _one = Owl_const.one k in
let lu = M.copy a in
let n = (M.shape a).(0) in
let m = (M.shape a).(1) in
assert (n = m);
let indx = Array.make n 0 in
(* implicit scaling of each row *)
let vv = Array.make n _zero in
let tiny = _flt 1.0e-40 in
let big = ref _zero in
let temp = ref _zero in
(* flag of row exchange *)
let d = ref 1.0 in
let imax = ref 0 in
(* loop over rows to get the implicit scaling information *)
for i = 0 to n - 1 do
big := _zero;
for j = 0 to n - 1 do
temp := M.get lu [| i; j |] |> _abs;
if !temp > !big then big := !temp
done;
if !big = _zero then raise Owl_exception.SINGULAR;
vv.(i) <- _div _one !big
done;
for k = 0 to n - 1 do
big := _zero;
(* choose suitable pivot *)
for i = k to n - 1 do
temp := _mul (M.get lu [| i; k |] |> _abs) vv.(i);
if !temp > !big
then (
big := !temp;
imax := i)
done;
(* interchange rows *)
if k <> !imax
then (
for j = 0 to n - 1 do
temp := M.get lu [| !imax; j |];
let tmp = M.get lu [| k; j |] in
M.set lu [| !imax; j |] tmp;
M.set lu [| k; j |] !temp
done;
d := !d *. -1.;
vv.(!imax) <- vv.(k));
indx.(k) <- !imax;
if M.get lu [| k; k |] = _zero then M.set lu [| k; k |] tiny;
for i = k + 1 to n - 1 do
let tmp0 = M.get lu [| i; k |] in
let tmp1 = M.get lu [| k; k |] in
temp := _div tmp0 tmp1;
M.set lu [| i; k |] !temp;
for j = k + 1 to n - 1 do
let prev = M.get lu [| i; j |] in
M.set lu [| i; j |] (_sub prev (_mul !temp (M.get lu [| k; j |])))
done
done
done;
lu, indx, !d
(* LU decomposition, return L, U, and permutation vector *)
let lu a =
let k = M.kind a in
let _zero = Owl_const.zero k in
let lu, indx, _ = _lu_base a in
let n = (M.shape lu).(0) in
let m = (M.shape lu).(1) in
assert (n = m && n >= 2);
let l = M.eye k n in
for r = 1 to n - 1 do
for c = 0 to r - 1 do
let v = M.get lu [| r; c |] in
M.set l [| r; c |] v;
M.set lu [| r; c |] _zero
done
done;
l, lu, indx
let _lu_solve_vec a b =
let _k = M.kind a in
let _mul = Owl_base_dense_common._mul_elt _k in
let _div = Owl_base_dense_common._div_elt _k in
let _sub = Owl_base_dense_common._sub_elt _k in
let _zero = Owl_const.zero _k in
assert (Array.length (M.shape b) = 1);
let n = (M.shape a).(0) in
if (M.shape b).(0) <> n then failwith "LUdcmp::solve bad sizes";
let ii = ref 0 in
let sum = ref _zero in
let x = M.copy b in
let lu, indx, _ = _lu_base a in
for i = 0 to n - 1 do
let ip = indx.(i) in
sum := M.get x [| ip |];
M.set x [| ip |] (M.get x [| i |]);
if !ii <> 0
then
for j = !ii - 1 to i - 1 do
sum := _sub !sum (_mul (M.get lu [| i; j |]) (M.get x [| j |]))
done
else if !sum <> _zero
then ii := !ii + 1;
M.set x [| i |] !sum
done;
for i = n - 1 downto 0 do
sum := M.get x [| i |];
for j = i + 1 to n - 1 do
sum := _sub !sum (_mul (M.get lu [| i; j |]) (M.get x [| j |]))
done;
M.set x [| i |] (_div !sum (M.get lu [| i; i |]))
done;
x
The _lu_base
function takes a square matrix a
as input and returns the L and U matrices, as well as a row permutation vector.
The function first makes a copy of the input matrix a
into a new matrix lu
.
It initializes some variables, including the row permutation vector indx
, the scaling vector vv
, a small value tiny
, and a flag d
.
The function then calculates the implicit scaling of each row by finding the maximum absolute value in each row and storing the reciprocal of that value in the vv
vector.
It then performs the LU decomposition using Gaussian elimination with partial pivoting. The function loops over each column k
and selects the pivot element as the one with the largest scaled value in the column. If necessary, it exchanges rows to bring the pivot element to the diagonal. It stores the row index of each pivot element in the indx
vector.
During the elimination process, the function stores the multipliers used to eliminate the elements below the pivot element in the lu
matrix.
The function also keeps track of the sign of the row exchanges in the d
flag.
After the elimination process is complete, the function returns the lu
matrix, the indx
vector, and the d
flag.
The lu
function calls _lu_base
and then formats the output into the L and U matrices. It first calls _lu_base
to get the lu
matrix, indx
vector, and d
flag. It then creates an identity matrix l
with the same size as lu
.
The function sets the elements of l
to the corresponding elements of lu
below the diagonal.
The function returns the l
and lu
matrices, as well as the indx
vector.
The _lu_solve_vec
function uses the output of _lu_base
to solve a system of linear equations. It takes a matrix a
and a vector b
as input and returns the solution vector x
.
The function first checks that the dimensions of a
and b
are compatible.
It then calls _lu_base
to get the lu
matrix, indx
vector, and d
flag.
The function initializes some variables and loops over each row of the lu
matrix to solve for the intermediate solution vector y
.
The function then loops over each row of the lu
matrix again to solve for the final solution vector x
.
The function returns the solution vector x
.
The time complexity of LU decomposition is O(n^3), where n is the size of the input matrix. This is because the algorithm involves performing Gaussian elimination on the matrix, which requires O(n^3) operations. The space complexity of the algorithm is also O(n^2), since it requires storing the L and U matrices, as well as the row permutation vector. However, the algorithm is numerically stable and is widely used in practice for solving systems of linear equations.
]]>