Initial commit.
This commit is contained in:
commit
a36add2344
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/maze.png
|
59
cmd/maze/main.go
Normal file
59
cmd/maze/main.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"smariot.com/tsp"
|
||||
"smariot.com/tsp/maze"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
w, h int
|
||||
seed int64
|
||||
imagePath string
|
||||
)
|
||||
|
||||
flag.IntVar(&w, "w", 20, "width of the maze")
|
||||
flag.IntVar(&h, "h", 15, "height of the maze")
|
||||
flag.StringVar(&imagePath, "o", "maze.png", "output image path")
|
||||
flag.Int64Var(&seed, "seed", rand.Int63(), "random seed")
|
||||
flag.Parse()
|
||||
|
||||
t := time.Now()
|
||||
m := maze.Make(image.Rect(0, 0, w, h), rand.New(rand.NewSource(seed)))
|
||||
log.Printf("created maze in %v", time.Since(t))
|
||||
|
||||
t = time.Now()
|
||||
path, err := tsp.Solve(maze.NewProblem(m.Size, m.Start, m.End, m.Right, m.Down), 0)
|
||||
log.Printf("solved maze in %v", time.Since(t))
|
||||
|
||||
if err != nil {
|
||||
log.Printf("failed to solve maze: %v", err)
|
||||
}
|
||||
|
||||
t = time.Now()
|
||||
image := m.Draw(path)
|
||||
log.Printf("drawn maze in %v", time.Since(t))
|
||||
|
||||
f, err := os.Create(imagePath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create image file: %v", err)
|
||||
}
|
||||
|
||||
t = time.Now()
|
||||
if err := png.Encode(f, image); err != nil {
|
||||
log.Fatalf("failed to write image: %v", err)
|
||||
}
|
||||
log.Printf("wrote image in %v", time.Since(t))
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("failed to close image file: %v", err)
|
||||
}
|
||||
}
|
73
internal/solver/bounded/bounded.go
Normal file
73
internal/solver/bounded/bounded.go
Normal file
@ -0,0 +1,73 @@
|
||||
package bounded
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
)
|
||||
|
||||
type minHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
items []State
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Len() int {
|
||||
return len(h.items)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.OptimisticLess(h.items[i], h.items[j])
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Swap(i, j int) {
|
||||
h.items[i], h.items[j] = h.items[j], h.items[i]
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Push(x any) {
|
||||
state := x.(State)
|
||||
h.items = append(h.items, state)
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Pop() any {
|
||||
n := len(h.items)
|
||||
state := h.items[n-1]
|
||||
h.items = h.items[:n-1]
|
||||
return state
|
||||
}
|
||||
|
||||
type solver[P problem.Problem[State], State comparable] struct {
|
||||
minHeap[P, State]
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Push(state State) {
|
||||
heap.Push(&s.minHeap, state)
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Pop() (State, bool) {
|
||||
if s.Len() == 0 {
|
||||
var zero State
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return heap.Pop(&s.minHeap).(State), true
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Reset() {
|
||||
for _, state := range s.items {
|
||||
s.problem.Discard(state)
|
||||
}
|
||||
|
||||
s.items = s.items[:0]
|
||||
}
|
||||
|
||||
// Returns a solver for bounded problems.
|
||||
//
|
||||
// This solver does not track states. Submitting a state multiple times will
|
||||
// result in multiple copies being stored, and multiple calls to problem.Discard.
|
||||
func New[P problem.Problem[State], State comparable](problem P) *solver[P, State] {
|
||||
return &solver[P, State]{
|
||||
minHeap: minHeap[P, State]{
|
||||
problem: problem,
|
||||
},
|
||||
}
|
||||
}
|
86
internal/solver/bounded_tracking/bounded_tracking.go
Normal file
86
internal/solver/bounded_tracking/bounded_tracking.go
Normal file
@ -0,0 +1,86 @@
|
||||
package bounded_tracking
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
)
|
||||
|
||||
type minHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
known map[State]int
|
||||
items []State
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Len() int {
|
||||
return len(h.items)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.OptimisticLess(h.items[i], h.items[j])
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Swap(i, j int) {
|
||||
h.items[i], h.items[j] = h.items[j], h.items[i]
|
||||
h.known[h.items[i]] = i
|
||||
h.known[h.items[j]] = j
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Push(x any) {
|
||||
state := x.(State)
|
||||
h.items = append(h.items, state)
|
||||
h.known[state] = len(h.items)
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Pop() any {
|
||||
n := len(h.items)
|
||||
state := h.items[n-1]
|
||||
h.items = h.items[:n-1]
|
||||
delete(h.known, state)
|
||||
return state
|
||||
}
|
||||
|
||||
type solver[P problem.Problem[State], State comparable] struct {
|
||||
minHeap[P, State]
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Push(state State) {
|
||||
if i, ok := s.known[state]; ok {
|
||||
// The state is already in the heap, update its position instead.
|
||||
heap.Fix(&s.minHeap, i)
|
||||
return
|
||||
}
|
||||
|
||||
heap.Push(&s.minHeap, state)
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Pop() (State, bool) {
|
||||
if s.Len() == 0 {
|
||||
var zero State
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return heap.Pop(&s.minHeap).(State), true
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Reset() {
|
||||
for _, state := range s.items {
|
||||
s.problem.Discard(state)
|
||||
}
|
||||
|
||||
s.items = s.items[:0]
|
||||
clear(s.known)
|
||||
}
|
||||
|
||||
// Returns a solver that for bounded problems that can update their states.
|
||||
//
|
||||
// Submitting a state that is already in the heap will update its position,
|
||||
// rather than adding it again. problem.Discard will only be invoked once.
|
||||
func New[P problem.Problem[State], State comparable](problem P) *solver[P, State] {
|
||||
return &solver[P, State]{
|
||||
minHeap: minHeap[P, State]{
|
||||
problem: problem,
|
||||
known: make(map[State]int),
|
||||
},
|
||||
}
|
||||
}
|
50
internal/solver/problem/problem.go
Normal file
50
internal/solver/problem/problem.go
Normal file
@ -0,0 +1,50 @@
|
||||
package problem
|
||||
|
||||
type Problem[State comparable] interface {
|
||||
// Discards the given state, allowing any resources associated with it to be released.
|
||||
Discard(state State)
|
||||
|
||||
// Returns true if the first state is more likely to be a better solution than the second state,
|
||||
// assuming the best case scenario. This is used to determine the best state to expand next.
|
||||
//
|
||||
// If this were to compare the distance traveled so far, this would be equivalent to Dijkstra's algorithm.
|
||||
//
|
||||
// If you added a heuristic to estimate the remaining distance, this would be equivalent to A*.
|
||||
//
|
||||
// For the traveling salesman problem, you might use traveled distance plus half of the remaining greedy tour length as a lower bound (optimistic) estimate.
|
||||
OptimisticLess(a State, b State) bool
|
||||
|
||||
// Returns true if the first state is more likely to be a better solution than the second state,
|
||||
// assuming the worst case scenario. This is used to determine if the worst state to discard when at capacity.
|
||||
//
|
||||
// This can be equivalent to OptimisticLess in many cases.
|
||||
//
|
||||
// For the traveling salesman problem, you might use traveled distance plus the remaining greedy tour length as an upper bound (pessimistic) estimate.
|
||||
//
|
||||
// You generally want to penalize states with a lot of uncertainty about their actual cost, especially
|
||||
// for something like the traveling salesman problem where finding the optimal solution is impractical.
|
||||
|
||||
// We could maybe handle finding the optimal TSP solution for 100 points, but definitely not 1000.
|
||||
// For that, you'd likely want to cluster your points (maybe using k-means) into a manageable number of groups,
|
||||
// and recursively solve the problem on each group.
|
||||
PessimisticLess(a State, b State) bool
|
||||
}
|
||||
|
||||
type ProblemStateUpdates[State comparable] interface {
|
||||
Problem[State]
|
||||
|
||||
// If this function returns true, then the solver needs to keep track of
|
||||
// known states so that it can update their relative order when they're resubmitted.
|
||||
//
|
||||
// If a problem doesn't implement this method, then true is assumed by default.
|
||||
StateUpdates() bool
|
||||
}
|
||||
|
||||
func RequiresStateUpdates[P Problem[State], State comparable](p Problem[State]) bool {
|
||||
if p, ok := p.(ProblemStateUpdates[State]); ok {
|
||||
return p.StateUpdates()
|
||||
}
|
||||
|
||||
// return true as a safe default.
|
||||
return true
|
||||
}
|
33
internal/solver/solver.go
Normal file
33
internal/solver/solver.go
Normal file
@ -0,0 +1,33 @@
|
||||
package solver
|
||||
|
||||
import (
|
||||
"smariot.com/tsp/internal/solver/bounded"
|
||||
"smariot.com/tsp/internal/solver/bounded_tracking"
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
"smariot.com/tsp/internal/solver/unbounded"
|
||||
"smariot.com/tsp/internal/solver/unbounded_tracking"
|
||||
)
|
||||
|
||||
type Solver[State comparable] interface {
|
||||
Push(State)
|
||||
Pop() (State, bool)
|
||||
Reset()
|
||||
}
|
||||
|
||||
// New creates a new state solver for the given problem and capacity.
|
||||
//
|
||||
// A capacity of 0 implies no limit, and we won't maintain a max heap.
|
||||
//
|
||||
// If P implements ProblemStateUpdates, then the solver will keep track of known states.
|
||||
func New[P problem.Problem[State], State comparable](p P, capacity int) Solver[State] {
|
||||
switch {
|
||||
case capacity == 0 && problem.RequiresStateUpdates[P](p):
|
||||
return bounded_tracking.New(p)
|
||||
case capacity == 0:
|
||||
return bounded.New(p)
|
||||
case problem.RequiresStateUpdates[P](p):
|
||||
return unbounded_tracking.New(p, capacity)
|
||||
default:
|
||||
return unbounded.New(p, capacity)
|
||||
}
|
||||
}
|
174
internal/solver/unbounded/unbounded.go
Normal file
174
internal/solver/unbounded/unbounded.go
Normal file
@ -0,0 +1,174 @@
|
||||
package unbounded
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
)
|
||||
|
||||
type heapEntry[State comparable] struct {
|
||||
state State
|
||||
minIndex int
|
||||
maxIndex int
|
||||
}
|
||||
|
||||
type minHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
entries []heapEntry[State]
|
||||
indexes []int
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Len() int {
|
||||
return len(h.indexes)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.OptimisticLess(h.entries[h.indexes[i]].state, h.entries[h.indexes[j]].state)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Swap(i, j int) {
|
||||
h.entries[h.indexes[i]].minIndex = j
|
||||
h.entries[h.indexes[j]].minIndex = i
|
||||
h.indexes[i], h.indexes[j] = h.indexes[j], h.indexes[i]
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Push(x any) {
|
||||
index := x.(int)
|
||||
h.entries[index].minIndex = len(h.indexes)
|
||||
h.indexes = append(h.indexes, index)
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Pop() any {
|
||||
n := len(h.indexes)
|
||||
index := h.indexes[n-1]
|
||||
h.entries[index].minIndex = -1
|
||||
h.indexes = h.indexes[:n-1]
|
||||
return index
|
||||
}
|
||||
|
||||
type maxHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
entries []heapEntry[State]
|
||||
indexes []int
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Len() int {
|
||||
return len(h.indexes)
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.PessimisticLess(h.entries[h.indexes[j]].state, h.entries[h.indexes[i]].state)
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Swap(i, j int) {
|
||||
h.entries[h.indexes[i]].maxIndex = j
|
||||
h.entries[h.indexes[j]].maxIndex = i
|
||||
h.indexes[i], h.indexes[j] = h.indexes[j], h.indexes[i]
|
||||
}
|
||||
|
||||
func (h *maxHeap[P, State]) Push(x any) {
|
||||
index := x.(int)
|
||||
h.entries[index].maxIndex = len(h.indexes)
|
||||
h.indexes = append(h.indexes, index)
|
||||
}
|
||||
|
||||
func (h *maxHeap[P, State]) Pop() any {
|
||||
n := len(h.indexes)
|
||||
index := h.indexes[n-1]
|
||||
h.entries[index].maxIndex = -1
|
||||
h.indexes = h.indexes[:n-1]
|
||||
return index
|
||||
}
|
||||
|
||||
type solver[P problem.Problem[State], State comparable] struct {
|
||||
minHeap[P, State]
|
||||
maxHeap maxHeap[P, State]
|
||||
free []int
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Push(state State) {
|
||||
if len(s.free) == 0 {
|
||||
// if this is worse than the worst state, discard it.
|
||||
if !s.problem.PessimisticLess(state, s.entries[s.maxHeap.indexes[0]].state) {
|
||||
s.problem.Discard(state)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, discard and replace the worst state.
|
||||
index := s.maxHeap.indexes[0]
|
||||
s.problem.Discard(s.entries[index].state)
|
||||
s.entries[index].state = state
|
||||
heap.Fix(&s.minHeap, s.entries[index].minIndex)
|
||||
heap.Fix(&s.maxHeap, 0)
|
||||
return
|
||||
}
|
||||
|
||||
index := s.free[len(s.free)-1]
|
||||
s.free = s.free[:len(s.free)-1]
|
||||
|
||||
s.entries[index].state = state
|
||||
|
||||
heap.Push(&s.minHeap, index)
|
||||
heap.Push(&s.maxHeap, index)
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Pop() (State, bool) {
|
||||
if s.Len() == 0 {
|
||||
var zero State
|
||||
return zero, false
|
||||
}
|
||||
|
||||
index := heap.Pop(&s.minHeap).(int)
|
||||
s.free = append(s.free, index)
|
||||
|
||||
heap.Remove(&s.maxHeap, s.entries[index].maxIndex)
|
||||
|
||||
return s.entries[index].state, true
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Reset() {
|
||||
for _, index := range s.minHeap.indexes {
|
||||
s.problem.Discard(s.entries[index].state)
|
||||
s.free = append(s.free, index)
|
||||
}
|
||||
|
||||
s.minHeap.indexes = s.minHeap.indexes[:0]
|
||||
s.maxHeap.indexes = s.maxHeap.indexes[:0]
|
||||
}
|
||||
|
||||
// Returns a solver for unbounded problems.
|
||||
//
|
||||
// It maintains both a min and a max heap, and will automatically discard states once it reaches a maximum capacity.
|
||||
//
|
||||
// It doesn't keep track of known states. Submitting a state multiple times will result in multiple copies being stored,
|
||||
// and problem.Discard being called multiple times.
|
||||
func New[P problem.Problem[State], State comparable](problem P, capacity int) *solver[P, State] {
|
||||
if capacity <= 0 {
|
||||
panic("unbounded.New: capacity must be greater than 0")
|
||||
}
|
||||
|
||||
free := make([]int, capacity)
|
||||
entries := make([]heapEntry[State], capacity)
|
||||
|
||||
for i := 0; i < capacity; i++ {
|
||||
free[i] = capacity - i - 1
|
||||
entries[i].minIndex = -1
|
||||
entries[i].maxIndex = -1
|
||||
}
|
||||
|
||||
indexes := make([]int, capacity*2)
|
||||
|
||||
return &solver[P, State]{
|
||||
free: free,
|
||||
minHeap: minHeap[P, State]{
|
||||
problem: problem,
|
||||
entries: entries,
|
||||
indexes: indexes[0:0:capacity],
|
||||
},
|
||||
maxHeap: maxHeap[P, State]{
|
||||
problem: problem,
|
||||
entries: entries,
|
||||
indexes: indexes[capacity : capacity : capacity*2],
|
||||
},
|
||||
}
|
||||
}
|
187
internal/solver/unbounded_tracking/unbounded_tracking.go
Normal file
187
internal/solver/unbounded_tracking/unbounded_tracking.go
Normal file
@ -0,0 +1,187 @@
|
||||
package unbounded_tracking
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
)
|
||||
|
||||
type heapEntry[State comparable] struct {
|
||||
state State
|
||||
minIndex int
|
||||
maxIndex int
|
||||
}
|
||||
|
||||
type minHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
entries []heapEntry[State]
|
||||
indexes []int
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Len() int {
|
||||
return len(h.indexes)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.OptimisticLess(h.entries[h.indexes[i]].state, h.entries[h.indexes[j]].state)
|
||||
}
|
||||
|
||||
func (h minHeap[P, State]) Swap(i, j int) {
|
||||
h.entries[h.indexes[i]].minIndex = j
|
||||
h.entries[h.indexes[j]].minIndex = i
|
||||
h.indexes[i], h.indexes[j] = h.indexes[j], h.indexes[i]
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Push(x any) {
|
||||
index := x.(int)
|
||||
h.entries[index].minIndex = len(h.indexes)
|
||||
h.indexes = append(h.indexes, index)
|
||||
}
|
||||
|
||||
func (h *minHeap[P, State]) Pop() any {
|
||||
n := len(h.indexes)
|
||||
index := h.indexes[n-1]
|
||||
h.entries[index].minIndex = -1
|
||||
h.indexes = h.indexes[:n-1]
|
||||
return index
|
||||
}
|
||||
|
||||
type maxHeap[P problem.Problem[State], State comparable] struct {
|
||||
problem problem.Problem[State]
|
||||
entries []heapEntry[State]
|
||||
indexes []int
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Len() int {
|
||||
return len(h.indexes)
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Less(i, j int) bool {
|
||||
return h.problem.PessimisticLess(h.entries[h.indexes[j]].state, h.entries[h.indexes[i]].state)
|
||||
}
|
||||
|
||||
func (h maxHeap[P, State]) Swap(i, j int) {
|
||||
h.entries[h.indexes[i]].maxIndex = j
|
||||
h.entries[h.indexes[j]].maxIndex = i
|
||||
h.indexes[i], h.indexes[j] = h.indexes[j], h.indexes[i]
|
||||
}
|
||||
|
||||
func (h *maxHeap[P, State]) Push(x any) {
|
||||
index := x.(int)
|
||||
h.entries[index].maxIndex = len(h.indexes)
|
||||
h.indexes = append(h.indexes, index)
|
||||
}
|
||||
|
||||
func (h *maxHeap[P, State]) Pop() any {
|
||||
n := len(h.indexes)
|
||||
index := h.indexes[n-1]
|
||||
h.entries[index].maxIndex = -1
|
||||
h.indexes = h.indexes[:n-1]
|
||||
return index
|
||||
}
|
||||
|
||||
type solver[P problem.Problem[State], State comparable] struct {
|
||||
minHeap[P, State]
|
||||
maxHeap maxHeap[P, State]
|
||||
known map[State]int
|
||||
free []int
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Push(state State) {
|
||||
if i, ok := s.known[state]; ok {
|
||||
// The state is already in the heap, update its position instead.
|
||||
heap.Fix(&s.minHeap, s.entries[i].minIndex)
|
||||
heap.Fix(&s.maxHeap, s.entries[i].maxIndex)
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.free) == 0 {
|
||||
// if this is worse than the worst state, discard it.
|
||||
if !s.problem.PessimisticLess(state, s.entries[s.maxHeap.indexes[0]].state) {
|
||||
s.problem.Discard(state)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, discard and replace the worst state.
|
||||
index := s.maxHeap.indexes[0]
|
||||
delete(s.known, s.entries[index].state)
|
||||
s.problem.Discard(s.entries[index].state)
|
||||
s.entries[index].state = state
|
||||
s.known[state] = index
|
||||
heap.Fix(&s.minHeap, s.entries[index].minIndex)
|
||||
heap.Fix(&s.maxHeap, 0)
|
||||
return
|
||||
}
|
||||
|
||||
index := s.free[len(s.free)-1]
|
||||
s.free = s.free[:len(s.free)-1]
|
||||
|
||||
s.entries[index].state = state
|
||||
s.known[state] = index
|
||||
|
||||
heap.Push(&s.minHeap, index)
|
||||
heap.Push(&s.maxHeap, index)
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Pop() (State, bool) {
|
||||
if s.Len() == 0 {
|
||||
var zero State
|
||||
return zero, false
|
||||
}
|
||||
|
||||
index := heap.Pop(&s.minHeap).(int)
|
||||
s.free = append(s.free, index)
|
||||
|
||||
heap.Remove(&s.maxHeap, s.entries[index].maxIndex)
|
||||
delete(s.known, s.entries[index].state)
|
||||
|
||||
return s.entries[index].state, true
|
||||
}
|
||||
|
||||
func (s *solver[P, State]) Reset() {
|
||||
for _, index := range s.minHeap.indexes {
|
||||
s.problem.Discard(s.entries[index].state)
|
||||
s.free = append(s.free, index)
|
||||
}
|
||||
|
||||
s.minHeap.indexes = s.minHeap.indexes[:0]
|
||||
s.maxHeap.indexes = s.maxHeap.indexes[:0]
|
||||
clear(s.known)
|
||||
}
|
||||
|
||||
// Returns a solver for unbounded problems, where states can be updated.
|
||||
//
|
||||
// It maintains both a min and a max heap, and will automatically discard states once it reaches a maximum capacity.
|
||||
//
|
||||
// Submitting a state that is already in the heap will update its position in the heap.
|
||||
func New[P problem.Problem[State], State comparable](problem P, capacity int) *solver[P, State] {
|
||||
if capacity <= 0 {
|
||||
panic("unbounded.New: capacity must be greater than 0")
|
||||
}
|
||||
|
||||
free := make([]int, capacity)
|
||||
entries := make([]heapEntry[State], capacity)
|
||||
|
||||
for i := 0; i < capacity; i++ {
|
||||
free[i] = capacity - i - 1
|
||||
entries[i].minIndex = -1
|
||||
entries[i].maxIndex = -1
|
||||
}
|
||||
|
||||
indexes := make([]int, capacity*2)
|
||||
|
||||
return &solver[P, State]{
|
||||
free: free,
|
||||
known: make(map[State]int),
|
||||
minHeap: minHeap[P, State]{
|
||||
problem: problem,
|
||||
entries: entries,
|
||||
indexes: indexes[0:0:capacity],
|
||||
},
|
||||
maxHeap: maxHeap[P, State]{
|
||||
problem: problem,
|
||||
entries: entries,
|
||||
indexes: indexes[capacity : capacity : capacity*2],
|
||||
},
|
||||
}
|
||||
}
|
269
maze/maze.go
Normal file
269
maze/maze.go
Normal file
@ -0,0 +1,269 @@
|
||||
package maze
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
type cell uint8
|
||||
|
||||
const (
|
||||
// cellRight is set if movement to the right is allowed.
|
||||
cellRight cell = 1 << iota
|
||||
|
||||
// cellDown is set if movement down is allowed.
|
||||
cellDown
|
||||
)
|
||||
|
||||
type Maze struct {
|
||||
Size image.Rectangle
|
||||
Start, End image.Point
|
||||
|
||||
// the cells in the maze, starting at size.Min and going left-to-right, top-to-bottom.
|
||||
cells []cell
|
||||
}
|
||||
|
||||
// if cells at i and j belong to different neighborhoods, they will be merged
|
||||
// and true will be returned. Otherwise, false will be returned.
|
||||
func tryJoinNeighborhoods(neighborhood []*[]int, i, j int) bool {
|
||||
if aN, bN := neighborhood[i], neighborhood[j]; aN != bN {
|
||||
// prefer the larger set absorbing the smaller set.
|
||||
if len(*aN) < len(*bN) {
|
||||
aN, bN = bN, aN
|
||||
}
|
||||
|
||||
// aN is now the larger set, and bN is the smaller set.
|
||||
|
||||
// append the smaller set to the larger set.
|
||||
*aN = append(*aN, *bN...)
|
||||
|
||||
// update the membership of the smaller set to point to the larger set.
|
||||
for _, member := range *bN {
|
||||
neighborhood[member] = aN
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Make creates a new maze of the given size, using the given random number generator.
|
||||
//
|
||||
// If r is nil, the default random number generator will be used.
|
||||
func Make(size image.Rectangle, r *rand.Rand) Maze {
|
||||
if size.Dx() <= 0 || size.Dy() <= 0 {
|
||||
panic("invalid maze size")
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
// create a new random number generator if
|
||||
// one wasn't provided.
|
||||
r = rand.New(rand.NewSource(rand.Int63()))
|
||||
}
|
||||
|
||||
w, h := size.Dx(), size.Dy()
|
||||
neighborhoods := make([]*[]int, w*h)
|
||||
cells := make([]cell, w*h)
|
||||
|
||||
// each cell begins initially disconnected, alone in its own set.
|
||||
for i := range cells {
|
||||
neighborhoods[i] = &[]int{i}
|
||||
}
|
||||
|
||||
// create a list of all edges in the maze.
|
||||
// there are (w-1)*h horizontal edges, and w*(h-1) vertical edges.
|
||||
// the first (w-1)*h edges are horizontal, and the rest are vertical.
|
||||
edges := make([]int, (w-1)*h+w*(h-1))
|
||||
for i := range edges {
|
||||
edges[i] = i
|
||||
}
|
||||
|
||||
// shuffle the list of edges, this will be the order in which we attempt to join cells.
|
||||
r.Shuffle(len(edges), func(i, j int) {
|
||||
edges[i], edges[j] = edges[j], edges[i]
|
||||
})
|
||||
|
||||
// repeatedly join cells until all cells are in the same set.
|
||||
for _, edge := range edges {
|
||||
if edge < (w-1)*h {
|
||||
// join right
|
||||
// we need to convert the edge index into coordinates, and then back into a cell index.
|
||||
i := edge%(w-1) + edge/(w-1)*w
|
||||
if tryJoinNeighborhoods(neighborhoods, i, i+1) {
|
||||
cells[i] |= cellRight
|
||||
} else {
|
||||
// already joined, skip to the next edge.
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// join down
|
||||
// we can mostly use the edge index unmodified, we just need to subtract the length of the horizontal edges.
|
||||
i := edge - (w-1)*h
|
||||
if tryJoinNeighborhoods(neighborhoods, i, i+w) {
|
||||
cells[i] |= cellDown
|
||||
} else {
|
||||
// already joined, skip to the next edge.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this point, then two neighborhoods were joined. If all cells are in the same neighborhood,
|
||||
// then we can stop trying to merge the remaining edges.
|
||||
if len(*neighborhoods[0]) == w*h {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var start, end image.Point
|
||||
|
||||
// choose a start and end point on opposite sides of the maze.
|
||||
switch r.Intn(2) {
|
||||
case 0: // left/right
|
||||
start = image.Pt(0, r.Intn(h)).Add(size.Min)
|
||||
end = image.Pt(w-1, r.Intn(h)).Add(size.Min)
|
||||
|
||||
case 1: // top/bottom
|
||||
start = image.Pt(r.Intn(w), 0).Add(size.Min)
|
||||
end = image.Pt(r.Intn(w), h-1).Add(size.Min)
|
||||
}
|
||||
|
||||
if r.Intn(2) == 0 {
|
||||
// swap start and end points.
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
return Maze{
|
||||
Size: size,
|
||||
Start: start,
|
||||
End: end,
|
||||
cells: cells,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Maze) coordToIndex(p image.Point) int {
|
||||
return p.X - m.Size.Min.X + (p.Y-m.Size.Min.Y)*m.Size.Dx()
|
||||
}
|
||||
|
||||
// Returns true if you can move to the right from the given point.
|
||||
func (m Maze) Right(p image.Point) bool {
|
||||
return p.In(m.Size) && m.cells[m.coordToIndex(p)]&cellRight != 0
|
||||
}
|
||||
|
||||
// Returns true if you can move down from the given point.
|
||||
func (m Maze) Down(p image.Point) bool {
|
||||
return p.In(m.Size) && m.cells[m.coordToIndex(p)]&cellDown != 0
|
||||
}
|
||||
|
||||
// Returns true if you can move to the left from the given point.
|
||||
func (m Maze) Left(p image.Point) bool {
|
||||
return m.Right(p.Sub(image.Pt(1, 0)))
|
||||
}
|
||||
|
||||
// Returns true if you can move up from the given point.
|
||||
func (m Maze) Up(p image.Point) bool {
|
||||
return m.Down(p.Sub(image.Pt(0, 1)))
|
||||
}
|
||||
|
||||
// Returns a drawing of the maze, mostly for debugging purposes.
|
||||
// An optional path can be drawn on top of the maze.
|
||||
func (m Maze) Draw(path []image.Point) *image.RGBA {
|
||||
const cellSize = 16
|
||||
const markerRadius = 4 // the radius of the start and end markers.
|
||||
const wallRadius = 2 // half the thickness of the wall lines.
|
||||
const pathRadius = 2 // half the thickness of the path line.
|
||||
|
||||
backgroundColor := image.NewUniform(color.Black)
|
||||
wallColor := image.NewUniform(color.White)
|
||||
startColor := image.NewUniform(color.RGBA{0, 255, 0, 255})
|
||||
endColor := image.NewUniform(color.RGBA{255, 0, 0, 255})
|
||||
pathColor := image.NewUniform(color.RGBA{0, 0, 255, 255})
|
||||
|
||||
leftWall := image.Rect(0, 0, 0, cellSize).Inset(-wallRadius)
|
||||
topWall := image.Rect(0, 0, cellSize, 0).Inset(-wallRadius)
|
||||
rightWall := leftWall.Add(image.Pt(cellSize, 0))
|
||||
bottomWall := topWall.Add(image.Pt(0, cellSize))
|
||||
|
||||
marker := image.Rect(cellSize/2-markerRadius, cellSize/2-markerRadius, cellSize/2+markerRadius, cellSize/2+markerRadius)
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, m.Size.Dx()*cellSize+wallRadius*2, m.Size.Dy()*cellSize+wallRadius*2))
|
||||
|
||||
// fill the entire image with the background color.
|
||||
draw.Draw(img, img.Bounds(), backgroundColor, image.Point{}, draw.Src)
|
||||
|
||||
for y := m.Size.Min.Y; y < m.Size.Max.Y; y++ {
|
||||
for x := m.Size.Min.X; x < m.Size.Max.X; x++ {
|
||||
|
||||
p := image.Pt(x, y)
|
||||
cellP := p.Sub(m.Size.Min).Mul(cellSize).Add(image.Pt(wallRadius, wallRadius))
|
||||
|
||||
draw := func(rect image.Rectangle, color *image.Uniform) {
|
||||
draw.Draw(img, rect.Add(cellP), color, image.Point{}, draw.Src)
|
||||
}
|
||||
|
||||
if p == m.Start {
|
||||
draw(marker, startColor)
|
||||
}
|
||||
|
||||
if p == m.End {
|
||||
draw(marker, endColor)
|
||||
}
|
||||
|
||||
if !m.Right(p) {
|
||||
draw(rightWall, wallColor)
|
||||
}
|
||||
|
||||
if !m.Down(p) {
|
||||
draw(bottomWall, wallColor)
|
||||
}
|
||||
|
||||
if !m.Left(p) {
|
||||
draw(leftWall, wallColor)
|
||||
}
|
||||
|
||||
if !m.Up(p) {
|
||||
draw(topWall, wallColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) > 1 {
|
||||
pathDrawOffset := image.Pt(cellSize/2+wallRadius, cellSize/2+wallRadius)
|
||||
|
||||
draw := func(i, j int) {
|
||||
// fake drawing a line between the two points by treating them as points
|
||||
// of a rectangle, and drawing that rectangle instead.
|
||||
//
|
||||
// this only works because we're assuming there are no diagonal movements.
|
||||
rect := image.Rectangle{
|
||||
path[i].Sub(m.Size.Min).Mul(cellSize),
|
||||
path[j].Sub(m.Size.Min).Mul(cellSize),
|
||||
}.Canon().Inset(-pathRadius).Add(pathDrawOffset)
|
||||
|
||||
draw.Draw(img, rect, pathColor, image.Point{}, draw.Src)
|
||||
}
|
||||
|
||||
start := 0
|
||||
|
||||
// cheating by assuming adjacent points in the path are adjacent cells,
|
||||
// and movements are only horizontal or vertical.
|
||||
for i := 1; i < len(path); i++ {
|
||||
if path[start].X == path[i].X || path[start].Y == path[i].Y {
|
||||
// while points are on the same horizontal or vertical line,
|
||||
// skip the intermediate points.
|
||||
continue
|
||||
}
|
||||
|
||||
// all the points before this one were on the same line, so draw them
|
||||
// as a single line.
|
||||
draw(start, i-1)
|
||||
start = i - 1
|
||||
}
|
||||
|
||||
// any remaining points we haven't drawn yet are also on the same line.
|
||||
draw(start, len(path)-1)
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
304
maze/problem.go
Normal file
304
maze/problem.go
Normal file
@ -0,0 +1,304 @@
|
||||
package maze
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"image"
|
||||
)
|
||||
|
||||
type problemCellState uint8
|
||||
|
||||
const (
|
||||
// zero value, for unvisited cells.
|
||||
stateUnvisited problemCellState = iota
|
||||
|
||||
// we considered the cell and set its heuristic, but then the movement
|
||||
// test failed, we don't know how to reach it still. distance is still uninitialized.
|
||||
stateUnexplored
|
||||
|
||||
// this is the starting cell.
|
||||
// distance is 0.
|
||||
stateStart
|
||||
|
||||
// the cell was reached via the respective direction.
|
||||
// distance is the number of cells traversed to reach this cell.
|
||||
stateRight
|
||||
stateDown
|
||||
stateLeft
|
||||
stateUp
|
||||
|
||||
// the number of directions, for array bounds.
|
||||
// not used as a state itself.
|
||||
stateLen
|
||||
)
|
||||
|
||||
var stateStrings = [stateLen]string{
|
||||
"unvisited",
|
||||
"unexplored",
|
||||
"start",
|
||||
"right",
|
||||
"down",
|
||||
"left",
|
||||
"up",
|
||||
}
|
||||
|
||||
// lookup table to reverse the direction.
|
||||
//
|
||||
// used to backtrack from the end to the start once the maze is solved.
|
||||
var dirReverse = [stateLen]problemCellState{
|
||||
stateUnvisited, // stateUnvisited
|
||||
stateUnexplored, // stateUnexplored
|
||||
stateStart, // stateStart
|
||||
stateLeft, // stateRight
|
||||
stateUp, // stateDown
|
||||
stateRight, // stateLeft
|
||||
stateDown, // stateUp
|
||||
}
|
||||
|
||||
// lookup table to advance in the given direction.
|
||||
var dirAdvance = [stateLen]image.Point{
|
||||
{0, 0}, // stateUnvisited
|
||||
{0, 0}, // stateUnexplored
|
||||
{0, 0}, // stateStart
|
||||
{1, 0}, // stateRight
|
||||
{0, 1}, // stateDown
|
||||
{-1, 0}, // stateLeft
|
||||
{0, -1}, // stateUp
|
||||
}
|
||||
|
||||
// lookup table to offset a point during movement tests
|
||||
// for left and up, we need to test the cell to the left or above.
|
||||
var dirTestOffset = [stateLen]image.Point{
|
||||
{0, 0}, // stateUnvisited
|
||||
{0, 0}, // stateUnexplored
|
||||
{0, 0}, // stateStart
|
||||
{0, 0}, // stateRight
|
||||
{0, 0}, // stateDown
|
||||
{-1, 0}, // stateLeft
|
||||
{0, -1}, // stateUp
|
||||
}
|
||||
|
||||
func (d problemCellState) String() string {
|
||||
if d < stateLen {
|
||||
return stateStrings[d]
|
||||
}
|
||||
|
||||
// The Stringer is the only case where we do bounds checking, the other functions can just panic.
|
||||
return fmt.Sprintf("unknown(%d)", d)
|
||||
}
|
||||
|
||||
type problemCell struct {
|
||||
state problemCellState
|
||||
|
||||
// the number of cells traversed to reach this cell.
|
||||
// only valid if state for stateStart, stateRight, stateDown, stateLeft, or stateUp.
|
||||
distance uint
|
||||
|
||||
// the optimistic distance estimate to the end point.
|
||||
// valid for any state except stateUnvisited.
|
||||
heuristic uint
|
||||
}
|
||||
|
||||
// A Problem is intended to be used by the Solve function to find a path through a maze.
|
||||
//
|
||||
// This is more of a demonstration of how to use the Solve function, a dedicated maze solving
|
||||
// algorithm would more efficient. In particular, the solver manages a max heap for discarding
|
||||
// states when at capacity, which a maze solver would not need - a maze has a finite number of cells,
|
||||
// which for our purposes represent a state, and we'd never exceed (and thus need to keep track
|
||||
// of and discard) that many states.
|
||||
type Problem struct {
|
||||
// the bounds of the maze.
|
||||
size image.Rectangle
|
||||
|
||||
// a lookup table for the bounds that a cell can be transverse in.
|
||||
//
|
||||
// for the movement directions, this will be identical to size, but contracted by one cell.
|
||||
// for example, legalPositions[stateRight] will have the rightmost column removed, as you can't
|
||||
// move further right from the rightmost column.
|
||||
//
|
||||
// for other possible states, the rectangle will be empty (and realistically, never used).
|
||||
legalPositions [stateLen]image.Rectangle
|
||||
|
||||
// the functions that test if movement is allowed in the given direction.
|
||||
// this needs to be combined with dirTestOffset to get the correct cell to test.
|
||||
testFuncs [stateLen]func(image.Point) bool
|
||||
|
||||
// the start and end points of the maze.
|
||||
start, end image.Point
|
||||
|
||||
// the cells in the maze, starting at size.Min, going left to right, top to bottom.
|
||||
cells []problemCell
|
||||
}
|
||||
|
||||
// convert a coordinate to an index in the cells slice.
|
||||
//
|
||||
// it is assumed that c.In(p.size) is true, otherwise this
|
||||
// will yield a wrong, and possibly out of bounds index.
|
||||
func (p Problem) coordToIndex(c image.Point) int {
|
||||
return (c.X - p.size.Min.X) + (c.Y-p.size.Min.Y)*p.size.Dx()
|
||||
}
|
||||
|
||||
func absDelta(a, b int) uint {
|
||||
if a > b {
|
||||
return uint(a - b)
|
||||
}
|
||||
return uint(b - a)
|
||||
}
|
||||
|
||||
// an optimistic distance estimate based on the Manhattan distance to the end point.
|
||||
func (p Problem) heuristic(c image.Point) uint {
|
||||
return absDelta(c.X, p.end.X) + absDelta(c.Y, p.end.Y)
|
||||
}
|
||||
|
||||
// Initializes the maze search problem, and appends the start point to the given slice.
|
||||
func (p *Problem) Initialize(out []image.Point) ([]image.Point, error) {
|
||||
p.cells = make([]problemCell, p.size.Dx()*p.size.Dy())
|
||||
|
||||
p.cells[p.coordToIndex(p.start)] = problemCell{
|
||||
state: stateStart,
|
||||
heuristic: p.heuristic(p.end),
|
||||
}
|
||||
|
||||
return append(out, p.start), nil
|
||||
}
|
||||
|
||||
// Appends the next positions to consider from the given position.
|
||||
func (p Problem) Next(c image.Point, out []image.Point) ([]image.Point, error) {
|
||||
distance := p.cells[p.coordToIndex(c)].distance + 1
|
||||
|
||||
for _, d := range [...]problemCellState{stateRight, stateDown, stateLeft, stateUp} {
|
||||
if !c.In(p.legalPositions[d]) {
|
||||
// can't move in this direction, stop immediately.
|
||||
continue
|
||||
}
|
||||
|
||||
next := c.Add(dirAdvance[d])
|
||||
nextIndex := p.coordToIndex(next)
|
||||
|
||||
switch {
|
||||
case p.cells[nextIndex].state == stateUnvisited:
|
||||
// calculate the heuristic for the first time and change its state to unexplored.
|
||||
p.cells[nextIndex].heuristic = p.heuristic(next)
|
||||
p.cells[nextIndex].state = stateUnexplored
|
||||
fallthrough
|
||||
|
||||
case p.cells[nextIndex].state == stateUnexplored:
|
||||
// similar to the unvisited case, but don't calculate the heuristic again.
|
||||
fallthrough
|
||||
|
||||
// if the cell has been visited, but we found a shorter path to it, then
|
||||
// update the direction and distance.
|
||||
case distance < p.cells[nextIndex].distance:
|
||||
// if we reach this point, then one of the above cases has occurred, and
|
||||
// this is a cell we're interested in exploring further.
|
||||
//
|
||||
// test to make sure we can actually move there.
|
||||
if p.testFuncs[d](c.Add(dirTestOffset[d])) {
|
||||
p.cells[nextIndex].state = d
|
||||
p.cells[nextIndex].distance = distance
|
||||
out = append(out, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Returns true if the given position is the end of the maze.
|
||||
//
|
||||
// We're also assuming that the point was returned by Initialize or Next,
|
||||
// otherwise we wouldn't know how to reach the start position, and this
|
||||
// wouldn't actually be solved.
|
||||
func (p Problem) Solved(c image.Point) bool {
|
||||
return c == p.end
|
||||
}
|
||||
|
||||
// Converts the given position, the one that passed the Solved test above,
|
||||
// into a list of points that form a path from the start to the end.
|
||||
func (p Problem) Finish(c image.Point) ([]image.Point, error) {
|
||||
if c != p.end {
|
||||
return nil, fmt.Errorf("not at the end: %v", c)
|
||||
}
|
||||
|
||||
switch p.cells[p.coordToIndex(c)].state {
|
||||
case stateUnvisited, stateUnexplored:
|
||||
return nil, fmt.Errorf("no path to the end")
|
||||
}
|
||||
|
||||
length := p.cells[p.coordToIndex(c)].distance
|
||||
path := make([]image.Point, length+1)
|
||||
path[length] = c
|
||||
|
||||
for ; length > 0; length-- {
|
||||
c = c.Add(dirAdvance[dirReverse[p.cells[p.coordToIndex(c)].state]])
|
||||
path[length-1] = c
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// This function would be used to release any resources associated with a given search state,
|
||||
// but since a state for this problem is just a point, and all the actual state information
|
||||
// is stored in the cells slice, there's nothing to do here.
|
||||
func (p Problem) Discard(image.Point) {
|
||||
// nop
|
||||
}
|
||||
|
||||
// Returns true if state a should be explored before b.
|
||||
func (p Problem) OptimisticLess(a, b image.Point) bool {
|
||||
ca := p.cells[p.coordToIndex(a)]
|
||||
cb := p.cells[p.coordToIndex(b)]
|
||||
|
||||
if r := cmp.Compare(ca.distance+ca.heuristic, cb.distance+cb.heuristic); r != 0 {
|
||||
return r < 0
|
||||
}
|
||||
|
||||
// If the estimated total path length for both states seems to be equal,
|
||||
// prefer the one with the lower heuristic (closest to the end).
|
||||
//
|
||||
// since distance+heuristic for both states is equal, this conversely means that
|
||||
// we prefer the one with the higher distance traveled so far.
|
||||
return ca.heuristic < cb.heuristic
|
||||
}
|
||||
|
||||
// Similar to OptimisticLess, but we scale the heuristic to penalize uncertainty
|
||||
// about the actual remaining distance.
|
||||
//
|
||||
// Note that the solver uses this with a max heap to determine which states to prune when at capacity.
|
||||
//
|
||||
// This will typically be similar to OptimisticLess, but with a penalty for uncertainty.
|
||||
//
|
||||
// For our purposes, we're just going to use the same heuristic as OptimisticLess. Assuming that you give
|
||||
// the solver a capacity equal to the number of cells in the maze, it won't need to prune any states.
|
||||
func (p Problem) PessimisticLess(a, b image.Point) bool {
|
||||
return p.OptimisticLess(a, b)
|
||||
}
|
||||
|
||||
// NewMazeProblem creates a new maze problem with the given size, start and end points,
|
||||
// and well as two functions that test if movement to the right and down is allowed from a given point.
|
||||
func NewProblem(size image.Rectangle, start, end image.Point, rightTest func(image.Point) bool, downTest func(image.Point) bool) *Problem {
|
||||
var legalPositions [stateLen]image.Rectangle
|
||||
|
||||
legalPositions[stateRight] = image.Rect(size.Min.X, size.Min.Y, size.Max.X-1, size.Max.Y)
|
||||
legalPositions[stateDown] = image.Rect(size.Min.X, size.Min.Y, size.Max.X, size.Max.Y-1)
|
||||
legalPositions[stateLeft] = image.Rect(size.Min.X+1, size.Min.Y, size.Max.X, size.Max.Y)
|
||||
legalPositions[stateUp] = image.Rect(size.Min.X, size.Min.Y+1, size.Max.X, size.Max.Y)
|
||||
|
||||
var testFuncs [stateLen]func(image.Point) bool
|
||||
|
||||
testFuncs[stateRight] = rightTest
|
||||
testFuncs[stateDown] = downTest
|
||||
|
||||
// note that the dirTestOffset lookup table has non-zero values for left and up,
|
||||
// to compensate for the fact that we need to test the cell to the left or above.
|
||||
testFuncs[stateLeft] = rightTest
|
||||
testFuncs[stateUp] = downTest
|
||||
|
||||
return &Problem{
|
||||
size: size,
|
||||
legalPositions: legalPositions,
|
||||
testFuncs: testFuncs,
|
||||
start: start,
|
||||
end: end,
|
||||
}
|
||||
}
|
108
solve.go
Normal file
108
solve.go
Normal file
@ -0,0 +1,108 @@
|
||||
package tsp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"smariot.com/tsp/internal/solver"
|
||||
"smariot.com/tsp/internal/solver/problem"
|
||||
)
|
||||
|
||||
// Problem represents a search problem.
|
||||
type Problem[State comparable, Solution any] interface {
|
||||
// Require a problem to implement the problem.Problem interface.
|
||||
problem.Problem[State]
|
||||
|
||||
// Appends the initial states to begin the search with.
|
||||
Initialize(out []State) ([]State, error)
|
||||
|
||||
// Appends the next possible states to the given slice.
|
||||
//
|
||||
// Appending an already known state is allowed, if this happens its
|
||||
// relative order is assumed to have changed.
|
||||
//
|
||||
// A state should only be resubmitted if its cost has changed, otherwise
|
||||
// we'd potentially be stuck in a loop trying to explore the same state over and over.
|
||||
Next(seed State, out []State) ([]State, error)
|
||||
|
||||
// Returns true if this node represents a complete solution.
|
||||
Solved(state State) bool
|
||||
|
||||
// Converts the given solved state into a solution that can be returned by the Solve function.
|
||||
Finish(state State) (Solution, error)
|
||||
}
|
||||
|
||||
// ErrNoSolution is returned by Solve when all states have been checked without finding a solution.
|
||||
var ErrNoSolution = errors.New("no solution found")
|
||||
|
||||
// ErrBadCapacity is returned by Solve when the capacity is not positive.
|
||||
var ErrBadCapacity = errors.New("capacity must be positive")
|
||||
|
||||
// Solves the given problem, returning the best state found.
|
||||
//
|
||||
// The capacity is the maximum number of states that can be stored in memory at any given time.
|
||||
//
|
||||
// If capacity is greater than 0, then the solver will maintain a max heap and discard states when it reaches capacity.
|
||||
func Solve[P Problem[State, Solution], State comparable, Solution any](problem P, capacity int) (Solution, error) {
|
||||
if capacity < 0 {
|
||||
var zero Solution
|
||||
return zero, fmt.Errorf("%w, got %d", ErrBadCapacity, capacity)
|
||||
}
|
||||
|
||||
solver := solver.New(problem, capacity)
|
||||
|
||||
next, err := problem.Initialize(nil)
|
||||
|
||||
// note that we push the states even in the case of an error,
|
||||
// as it's simplifies the cleanup process.
|
||||
for _, state := range next {
|
||||
solver.Push(state)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
solver.Reset()
|
||||
var zero Solution
|
||||
return zero, err
|
||||
}
|
||||
|
||||
for {
|
||||
state, ok := solver.Pop()
|
||||
|
||||
if !ok {
|
||||
var zero Solution
|
||||
return zero, ErrNoSolution
|
||||
}
|
||||
|
||||
if problem.Solved(state) {
|
||||
solver.Reset()
|
||||
solution, err := problem.Finish(state)
|
||||
problem.Discard(state)
|
||||
return solution, err
|
||||
}
|
||||
|
||||
next, err = problem.Next(state, next[:0])
|
||||
|
||||
// again, we're going to deal with the states we were given first
|
||||
// before we deal with the error.
|
||||
resubmitted := false
|
||||
for _, nextState := range next {
|
||||
if state == nextState {
|
||||
resubmitted = true
|
||||
}
|
||||
|
||||
solver.Push(nextState)
|
||||
}
|
||||
|
||||
// problem is allowed to resubmit the state. If it did, then it's either somewhere in the heap,
|
||||
// or it already discarded it due to being at capacity. In either case, we shouldn't discard it again.
|
||||
if !resubmitted {
|
||||
problem.Discard(state)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
solver.Reset()
|
||||
var zero Solution
|
||||
return zero, err
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user