Initial commit.
This commit is contained in:
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],
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user