From e33b5b790f0b6dede7e8bd1ede4c96bbca526a8b Mon Sep 17 00:00:00 2001 From: oabrivard Date: Thu, 25 Jan 2024 11:11:48 +0100 Subject: [PATCH] Added graph search adlgorithms and Heap and PriorityQueue data structures --- container/heap.go | 103 ++++++++++++++++ container/heap_test.go | 145 +++++++++++++++++++++++ container/priority_queue.go | 38 ++++++ container/priority_queue_test.go | 72 +++++++++++ graph/graph.go | 103 +++++++++++++++- graph/graph_test.go | 58 ++++++++- {math => maths}/geometry.go | 2 +- {math => maths}/geometry_test.go | 2 +- math/math.go => maths/maths.go | 6 +- math/math_test.go => maths/maths_test.go | 2 +- {math => maths}/matrix.go | 2 +- {math => maths}/matrix_test.go | 2 +- 12 files changed, 524 insertions(+), 11 deletions(-) create mode 100644 container/heap.go create mode 100644 container/heap_test.go create mode 100644 container/priority_queue.go create mode 100644 container/priority_queue_test.go rename {math => maths}/geometry.go (99%) rename {math => maths}/geometry_test.go (98%) rename math/math.go => maths/maths.go (97%) rename math/math_test.go => maths/maths_test.go (99%) rename {math => maths}/matrix.go (99%) rename {math => maths}/matrix_test.go (99%) diff --git a/container/heap.go b/container/heap.go new file mode 100644 index 0000000..2cd9846 --- /dev/null +++ b/container/heap.go @@ -0,0 +1,103 @@ +package container + +import ( + "golang.org/x/exp/constraints" +) + +type HeapItem[T any, V constraints.Integer] struct { + Value T + Priority V +} + +// Heap represents a generic heap data structure. +type Heap[T any, V constraints.Integer] struct { + elements []HeapItem[T, V] + isMinHeap bool +} + +// NewHeap creates a new heap with the specified ordering. +// The isMinHeap parameter determines whether the heap is a min heap or a max heap. +// The heap is initially empty. +func NewHeap[T any, V constraints.Integer](isMinHeap bool) *Heap[T, V] { + return &Heap[T, V]{elements: []HeapItem[T, V]{}, isMinHeap: isMinHeap} +} + +// Push adds an element to the heap. +func (h *Heap[T, V]) Push(element T, priority V) { + h.elements = append(h.elements, HeapItem[T, V]{element, priority}) + h.heapifyUp() +} + +// Pop removes and returns the root element from the heap. +func (h *Heap[T, V]) Pop() T { + if len(h.elements) == 0 { + var zero T // Return zero value of T + return zero + } + + root := h.elements[0] + lastIndex := len(h.elements) - 1 + h.elements[0] = h.elements[lastIndex] + h.elements = h.elements[:lastIndex] + + h.heapifyDown() + return root.Value +} + +// Peek returns the root element without removing it. +func (h *Heap[T, V]) Peek() T { + if len(h.elements) == 0 { + var zero T // Return zero value of T + return zero + } + return h.elements[0].Value +} + +// heapifyUp adjusts the heap after adding a new element. +func (h *Heap[T, V]) heapifyUp() { + index := len(h.elements) - 1 + for index > 0 { + parentIndex := (index - 1) / 2 + if h.compare(h.elements[index].Priority, h.elements[parentIndex].Priority) { + h.elements[index], h.elements[parentIndex] = h.elements[parentIndex], h.elements[index] + index = parentIndex + } else { + break + } + } +} + +// heapifyDown adjusts the heap after removing the root element. +func (h *Heap[T, V]) heapifyDown() { + index := 0 + lastIndex := len(h.elements) - 1 + for index < lastIndex { + leftChildIndex := 2*index + 1 + rightChildIndex := 2*index + 2 + + var childIndex int + if leftChildIndex <= lastIndex { + childIndex = leftChildIndex + if rightChildIndex <= lastIndex && h.compare(h.elements[rightChildIndex].Priority, h.elements[leftChildIndex].Priority) { + childIndex = rightChildIndex + } + + if h.compare(h.elements[childIndex].Priority, h.elements[index].Priority) { + h.elements[index], h.elements[childIndex] = h.elements[childIndex], h.elements[index] + index = childIndex + } else { + break + } + } else { + break + } + } +} + +// compare compares two elements based on the heap type. +func (h *Heap[T, V]) compare(x, y V) bool { + if h.isMinHeap { + return x < y + } + return x > y +} diff --git a/container/heap_test.go b/container/heap_test.go new file mode 100644 index 0000000..06ab3d9 --- /dev/null +++ b/container/heap_test.go @@ -0,0 +1,145 @@ +package container + +import ( + "testing" +) + +// TestPush tests the Push method of the Heap. +func TestPush(t *testing.T) { + heap := NewHeap[int, int](true) // Testing a min heap + heap.Push(3, 3) + heap.Push(1, 1) + heap.Push(2, 2) + + expected := []int{1, 3, 2} // The expected min-heap state after pushing elements + for i, v := range heap.elements { + if v.Value != expected[i] { + t.Errorf("Push() test failed: expected %v at index %d, got %v", expected[i], i, v) + } + } +} + +// TestPop tests the Pop method of the Heap. +func TestPop(t *testing.T) { + heap := NewHeap[int, int](true) + heap.Push(3, 3) + heap.Push(1, 1) + heap.Push(2, 2) + + if val := heap.Pop(); val != 1 { + t.Errorf("Pop() test failed: expected 1, got %v", val) + } + + if val := heap.Pop(); val != 2 { + t.Errorf("Pop() test failed: expected 2, got %v", val) + } +} + +// TestPeek tests the Peek method of the Heap. +func TestPeek(t *testing.T) { + heap := NewHeap[int, int](true) + heap.Push(3, 3) + heap.Push(1, 1) + heap.Push(2, 2) + + if val := heap.Peek(); val != 1 { + t.Errorf("Peek() test failed: expected 1, got %v", val) + } + + // Check if Peek() doesn't remove the element + if val := heap.Pop(); val != 1 { + t.Errorf("Peek() test failed: Peek() should not remove the element") + } +} + +// TestHeapProperty tests if the heap maintains its property after operations. +func TestHeapProperty(t *testing.T) { + heap := NewHeap[int, int](true) // Testing a min heap + heap.Push(5, 5) + heap.Push(3, 3) + heap.Push(8, 8) + heap.Push(1, 1) + heap.Push(7, 7) + + // After every Pop(), the next smallest element should come out + expectedOrder := []int{1, 3, 5, 7, 8} + for _, expected := range expectedOrder { + if val := heap.Pop(); val != expected { + t.Errorf("Heap property test failed: expected %v, got %v", expected, val) + } + } +} + +// TestNewHeap tests the NewHeap function. +func TestNewHeap(t *testing.T) { + // Test creating a min heap + minHeap := NewHeap[int, int](true) + if minHeap == nil { + t.Error("NewHeap() test failed: expected a non-nil heap") + } + if minHeap != nil && !minHeap.isMinHeap { + t.Error("NewHeap() test failed: expected a min heap") + } + + // Test creating a max heap + maxHeap := NewHeap[int, int](false) + if maxHeap == nil { + t.Error("NewHeap() test failed: expected a non-nil heap") + } + if minHeap != nil && maxHeap.isMinHeap { + t.Error("NewHeap() test failed: expected a max heap") + } +} + +// TestHeapifyUp tests the heapifyUp method of the Heap. +func TestHeapifyUp(t *testing.T) { + heap := NewHeap[int, int](true) + heap.elements = []HeapItem[int, int]{{2, 2}, {3, 3}, {1, 1}} + + heap.heapifyUp() + + expected := []int{1, 3, 2} // The expected min-heap state after heapifyUp + for i, v := range heap.elements { + if v.Value != expected[i] { + t.Errorf("heapifyUp() test failed: expected %v at index %d, got %v", expected[i], i, v) + } + } +} + +// TestHeapifyUpEmptyHeap tests the heapifyUp method of an empty Heap. +func TestHeapifyUpEmptyHeap(t *testing.T) { + heap := NewHeap[int, int](true) + + heap.heapifyUp() + + if len(heap.elements) != 0 { + t.Errorf("heapifyUp() test failed: expected empty heap, got %v", heap.elements) + } +} + +// TestHeapifyUpSingleElement tests the heapifyUp method of a Heap with a single element. +func TestHeapifyUpSingleElement(t *testing.T) { + heap := NewHeap[int, int](true) + heap.elements = []HeapItem[int, int]{{1, 1}} + + heap.heapifyUp() + + if len(heap.elements) != 1 || heap.elements[0].Value != 1 { + t.Errorf("heapifyUp() test failed: expected [1], got %v", heap.elements) + } +} + +// TestHeapifyDown tests the heapifyDown method of the Heap. +func TestHeapifyDown(t *testing.T) { + heap := NewHeap[int, int](true) + heap.elements = []HeapItem[int, int]{{8, 8}, {1, 1}, {3, 3}, {5, 5}, {7, 7}} + + heap.heapifyDown() + + expected := []int{1, 5, 3, 8, 7} // The expected heap state after heapifyDown + for i, v := range heap.elements { + if v.Value != expected[i] { + t.Errorf("heapifyDown() test failed: expected %v at index %d, got %v", expected[i], i, v) + } + } +} diff --git a/container/priority_queue.go b/container/priority_queue.go new file mode 100644 index 0000000..68ea0f9 --- /dev/null +++ b/container/priority_queue.go @@ -0,0 +1,38 @@ +package container + +import ( + "golang.org/x/exp/constraints" +) + +// PriorityQueue represents a priority queue data structure. +type PriorityQueue[T any, V constraints.Integer] struct { + heap *Heap[T, V] +} + +// NewPriorityQueue creates a new PriorityQueue instance. +// isMinQueue determines whether it is a min-priority queue (true) or a max-priority queue (false). +func NewPriorityQueue[T any, V constraints.Integer](isMinQueue bool) *PriorityQueue[T, V] { + return &PriorityQueue[T, V]{ + heap: NewHeap[T, V](isMinQueue), + } +} + +// Enqueue adds an element to the priority queue. +func (pq *PriorityQueue[T, V]) Enqueue(element T, priority V) { + pq.heap.Push(element, priority) +} + +// Dequeue removes and returns the element with the highest priority from the queue. +func (pq *PriorityQueue[T, V]) Dequeue() T { + return pq.heap.Pop() +} + +// Peek returns the element with the highest priority without removing it from the queue. +func (pq *PriorityQueue[T, V]) Peek() T { + return pq.heap.Peek() +} + +// IsEmpty returns true if the priority queue is empty. +func (pq *PriorityQueue[T, V]) IsEmpty() bool { + return len(pq.heap.elements) == 0 +} diff --git a/container/priority_queue_test.go b/container/priority_queue_test.go new file mode 100644 index 0000000..7451e17 --- /dev/null +++ b/container/priority_queue_test.go @@ -0,0 +1,72 @@ +package container + +import ( + "testing" +) + +// TestEnqueue tests the Enqueue method of the PriorityQueue. +func TestEnqueue(t *testing.T) { + pq := NewPriorityQueue[int, int](true) // Min-priority queue + pq.Enqueue(3, 3) + pq.Enqueue(1, 1) + pq.Enqueue(2, 2) + + expected := []int{1, 3, 2} // Expected min-heap state after enqueuing elements + for i, v := range pq.heap.elements { + if v.Value != expected[i] { + t.Errorf("Enqueue() test failed: expected %v at index %d, got %v", expected[i], i, v) + } + } +} + +// TestDequeue tests the Dequeue method of the PriorityQueue. +func TestDequeue(t *testing.T) { + pq := NewPriorityQueue[int, int](true) + pq.Enqueue(3, 3) + pq.Enqueue(1, 1) + pq.Enqueue(2, 2) + + if val := pq.Dequeue(); val != 1 { + t.Errorf("Dequeue() test failed: expected 1, got %v", val) + } + + if val := pq.Dequeue(); val != 2 { + t.Errorf("Dequeue() test failed: expected 2, got %v", val) + } +} + +// TestPeek tests the Peek method of the PriorityQueue. +func TestPQueuePeek(t *testing.T) { + pq := NewPriorityQueue[int, int](true) + pq.Enqueue(3, 3) + pq.Enqueue(1, 1) + pq.Enqueue(2, 2) + + if val := pq.Peek(); val != 1 { + t.Errorf("Peek() test failed: expected 1, got %v", val) + } + + // Check if Peek() doesn't remove the element + if val := pq.Dequeue(); val != 1 { + t.Errorf("Peek() test failed: Peek() should not remove the element") + } +} + +// TestIsEmpty tests the IsEmpty method of the PriorityQueue. +func TestIsEmpty(t *testing.T) { + pq := NewPriorityQueue[int, int](true) + + if !pq.IsEmpty() { + t.Errorf("IsEmpty() test failed: expected true, got false") + } + + pq.Enqueue(1, 1) + if pq.IsEmpty() { + t.Errorf("IsEmpty() test failed: expected false, got true") + } + + pq.Dequeue() + if !pq.IsEmpty() { + t.Errorf("IsEmpty() test failed: expected true, got false") + } +} diff --git a/graph/graph.go b/graph/graph.go index ff972de..bdafe7f 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -11,6 +11,7 @@ import ( "gitea.paas.celticinfo.fr/oabrivard/abrolgo/algo" "gitea.paas.celticinfo.fr/oabrivard/abrolgo/container" + "gitea.paas.celticinfo.fr/oabrivard/abrolgo/maths" "golang.org/x/exp/constraints" ) @@ -51,13 +52,13 @@ func (g *Graph[T, V]) AddVertex(vertex T) { } // AddEdge adds an edge to the graph between the source node (src) and the destination node (dest), -// with the given distance (dist). +// with the given weight (weight). // It adds the edge in both directions (src -> dest and dest -> src). // If vertices src and dest do not exist in the graph, they are added to the graph. -func (g *Graph[T, V]) AddEdge(src, dest T, dist V) { +func (g *Graph[T, V]) AddEdge(src, dest T, weight V) { g.edgeID++ - srcToDest := Edge[T, V]{g.edgeID, src, dest, dist} - destToSrc := Edge[T, V]{g.edgeID, dest, src, dist} + srcToDest := Edge[T, V]{g.edgeID, src, dest, weight} + destToSrc := Edge[T, V]{g.edgeID, dest, src, weight} g.adjacencyList[src] = append(g.adjacencyList[src], srcToDest) g.adjacencyList[dest] = append(g.adjacencyList[dest], destToSrc) g.edges = append(g.edges, srcToDest) @@ -510,3 +511,97 @@ func (g *Graph[T, V]) BFS(start T, isGoal func(vertex T) bool) []T { return []T{} } + +// AStar performs the A* search algorithm to find the shortest path between two vertices in the graph. +// It returns the shortest path as a slice of vertices. +// If no path is found, an empty slice is returned. +func (g *Graph[T, V]) AStar(start, goal T, heuristic func(vertex T) V) []T { + // Initialize the distance map + dist := map[T]V{} + for vertex := range g.adjacencyList { + dist[vertex] = maths.MaxInteger[V]() + } + dist[start] = V(0) + + // Initialize the previous map + prev := map[T]T{} + + // Initialize the priority queue + pq := container.NewPriorityQueue[T, V](true) + pq.Enqueue(start, 0) + + for !pq.IsEmpty() { + curVert := pq.Dequeue() + + if curVert == goal { + // Reconstruct the path + path := []T{goal} + for prevVertex, ok := prev[goal]; ok; prevVertex, ok = prev[prevVertex] { + path = append([]T{prevVertex}, path...) + } + return path + } + + for _, linkedNode := range g.adjacencyList[curVert] { + alt := dist[curVert] + linkedNode.weight + if alt < dist[linkedNode.dest] { + dist[linkedNode.dest] = alt + prev[linkedNode.dest] = curVert + priority := alt + heuristic(linkedNode.dest) + pq.Enqueue(linkedNode.dest, priority) + } + } + } + + return []T{} +} + +// Dijkstra performs Dijkstra's algorithm to find the shortest path between two vertices in the graph. +// It returns the shortest path as a slice of vertices. +// If no path is found, an empty slice is returned. +func (g *Graph[T, V]) Dijkstra(start, goal T) []T { + + return g.AStar(start, goal, func(vertex T) V { + return V(0) + }) + + /* + // Initialize the distance map + dist := map[T]V{} + for vertex := range g.adjacencyList { + dist[vertex] = maths.MaxInteger[V]() + } + dist[start] = V(0) + + // Initialize the previous map + prev := map[T]T{} + + // Initialize the priority queue + pq := container.NewPriorityQueue[T, V](true) + pq.Enqueue(start, 0) + + for !pq.IsEmpty() { + curVert := pq.Dequeue() + + if curVert == goal { + // Reconstruct the path + path := []T{goal} + for prevVertex, ok := prev[goal]; ok; prevVertex, ok = prev[prevVertex] { + path = append([]T{prevVertex}, path...) + } + return path + } + + for _, linkedNode := range g.adjacencyList[curVert] { + alt := dist[curVert] + linkedNode.weight + if alt < dist[linkedNode.dest] { + dist[linkedNode.dest] = alt + prev[linkedNode.dest] = curVert + pq.Enqueue(linkedNode.dest, alt) + } + } + } + + return []T{} + */ +} diff --git a/graph/graph_test.go b/graph/graph_test.go index 13c06e7..5c2d4d4 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -599,7 +599,7 @@ func TestKargerMinCutUF(t *testing.T) { g.AddEdge(5, 6, 70) // Run Karger's algorithm to find the minimum cut - cutEdges, resultSubsets := g.kargerMinCutUF(10000) + cutEdges, resultSubsets := g.kargerMinCutUF(100) // Check if the number of cut edges is correct expectedCutEdges := 1 @@ -722,3 +722,59 @@ func TestDFS(t *testing.T) { t.Errorf("DFS() failed with goal function: expected %v, got %v", expectedResult, result) } } +func TestDijkstra(t *testing.T) { + g := NewGraph[int, int]() + + // Add vertices and edges to the graph + g.AddEdge(1, 2, 10) + g.AddEdge(1, 3, 20) + g.AddEdge(2, 3, 30) + g.AddEdge(2, 4, 40) + g.AddEdge(3, 4, 50) + + // Test Dijkstra algorithm + start := 1 + goal := 4 + expectedPath := []int{1, 2, 4} + path := g.Dijkstra(start, goal) + + if !equalSlices(path, expectedPath) { + t.Errorf("Dijkstra() failed: expected path = %v, got path = %v", expectedPath, path) + } +} + +func TestAStar(t *testing.T) { + g := NewGraph[int, int]() + + // Add vertices and edges to the graph + g.AddEdge(1, 2, 10) + g.AddEdge(1, 3, 20) + g.AddEdge(2, 3, 30) + g.AddEdge(2, 4, 40) + g.AddEdge(3, 4, 50) + g.AddEdge(3, 5, 60) + g.AddEdge(4, 5, 70) + + // Define the heuristic function + heuristic := func(vertex int) int { + return 0 // equivalent of Disjkstra's algorithm + } + + // Test AStar for a valid path + start := 1 + goal := 5 + expectedPath := []int{1, 3, 5} + path := g.AStar(start, goal, heuristic) + if !equalSlices(path, expectedPath) { + t.Errorf("AStar() failed for valid path: expected %v, got %v", expectedPath, path) + } + + // Test AStar for an invalid path + start = 1 + goal = 6 + expectedPath = []int{} + path = g.AStar(start, goal, heuristic) + if !equalSlices(path, expectedPath) { + t.Errorf("AStar() failed for invalid path: expected %v, got %v", expectedPath, path) + } +} diff --git a/math/geometry.go b/maths/geometry.go similarity index 99% rename from math/geometry.go rename to maths/geometry.go index 4a18933..b8e1b9e 100644 --- a/math/geometry.go +++ b/maths/geometry.go @@ -1,4 +1,4 @@ -package math +package maths import "math" diff --git a/math/geometry_test.go b/maths/geometry_test.go similarity index 98% rename from math/geometry_test.go rename to maths/geometry_test.go index 2865c30..1067fe7 100644 --- a/math/geometry_test.go +++ b/maths/geometry_test.go @@ -1,4 +1,4 @@ -package math +package maths import "testing" diff --git a/math/math.go b/maths/maths.go similarity index 97% rename from math/math.go rename to maths/maths.go index 5f21411..fe00dfd 100644 --- a/math/math.go +++ b/maths/maths.go @@ -1,4 +1,4 @@ -package math +package maths import ( "golang.org/x/exp/constraints" @@ -111,3 +111,7 @@ func gaussianElimination(coefficients Matrix[float64], rhs []float64) { } } } + +func MaxInteger[T constraints.Integer]() T { + return T(^uint(T(0)) >> 1) +} diff --git a/math/math_test.go b/maths/maths_test.go similarity index 99% rename from math/math_test.go rename to maths/maths_test.go index 3a9a3ee..48a06e2 100644 --- a/math/math_test.go +++ b/maths/maths_test.go @@ -1,4 +1,4 @@ -package math +package maths import ( "testing" diff --git a/math/matrix.go b/maths/matrix.go similarity index 99% rename from math/matrix.go rename to maths/matrix.go index 033064b..55eaca4 100644 --- a/math/matrix.go +++ b/maths/matrix.go @@ -1,4 +1,4 @@ -package math +package maths type Matrix[T comparable] [][]T diff --git a/math/matrix_test.go b/maths/matrix_test.go similarity index 99% rename from math/matrix_test.go rename to maths/matrix_test.go index 52eb402..c1279fc 100644 --- a/math/matrix_test.go +++ b/maths/matrix_test.go @@ -1,4 +1,4 @@ -package math +package maths import ( "reflect"