Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
TSP attempts to find a closed loop of minimum length that visits each input point exactly once.
The general workflow is:
- Create a basic data structure with
inputPath
ordistances
+tspNearestNeighbourPath
- Improve it using
tsp2optPath
,tsp3optPath
- Retrieve the optimized geometry with
pathToPoints
Synopsis
- distances :: Vector Vec2 -> Distances
- data Distances
- pathToPoints :: Vector vec2 -> Vector TspPoint -> Vector vec2
- data TspPoint
- inputPath :: Vector a -> Vector TspPoint
- tspNearestNeighbourPath :: Distances -> Vector TspPoint
- tsp2optPath :: Distances -> Vector TspPoint -> Vector TspPoint
- tsp2optInplace :: PrimMonad m => Distances -> MVector (PrimState m) TspPoint -> m ()
- unsafePlan2optSwap :: PrimMonad m => Distances -> Int -> Int -> MVector (PrimState m) TspPoint -> m Double
- unsafeCommit2optSwap :: PrimMonad m => Int -> Int -> MVector (PrimState m) a -> m ()
- tsp3optPath :: Distances -> Vector TspPoint -> Vector TspPoint
- tsp3optInplace :: PrimMonad m => Distances -> MVector (PrimState m) TspPoint -> m ()
- data Case3opt
- unsafePlan3optSwap :: PrimMonad m => Distances -> Int -> Int -> Int -> MVector (PrimState m) TspPoint -> m (Maybe (Case3opt, Double))
- unsafeCommit3optSwap :: PrimMonad m => Case3opt -> Int -> Int -> Int -> Int -> MVector (PrimState m) a -> m ()
Setup
distances :: Vector Vec2 -> Distances Source #
Cache the distance between points i and j.
The Vec2
s can be forgotten now, because we’re only interested in topological
properties, not the coordinates themselves. Once the calculation is done, the
result permutation can be obtained with pathToPoints
.
Cache for the (symmetrical) distance between points. See distances
.
pathToPoints :: Vector vec2 -> Vector TspPoint -> Vector vec2 Source #
Permute the input vector with the result of a TSP optimization.
Abstract point in the TSP algorithms. Corresponds to the \(i\)-th input point.
Construction
inputPath :: Vector a -> Vector TspPoint Source #
Interpret the input as a path as-is. The simplest possible TSP solver. :-)
(image code)
>>>
:{
haddockRender "Numerics/Optimization/TSP/input_path.svg" 300 200 $ \_ -> do points <- liftIO $ do gen <- MWC.initialize (V.fromList []) uniform <- uniformlyDistributedPoints gen (shrinkBoundingBox 10 [zero, Vec2 300 200]) 32 pure uniform setLineJoin LineJoinRound let path = inputPath points tour = Polygon (V.toList (pathToPoints points path)) cairoScope $ do setColor (mma 0) for_ points $ \p -> sketch (Circle p 3) >> fill cairoScope $ do setColor (mma 1) sketch tour stroke :} Generated file: size 11KB, crc32: 0xde10a233
tspNearestNeighbourPath :: Distances -> Vector TspPoint Source #
Starting at the \(i\)-th point, create a path by taking the nearest neighbour. This algorithm is very simple and fast, but yields surprisingly short paths for everyday inputs.
It is an excellent starting point for more accurate but slower algorithms, such
as tsp2optPath
.
(image code)
>>>
:{
haddockRender "Numerics/Optimization/TSP/nearest_neighbour_path.svg" 300 200 $ \_ -> do points <- liftIO $ do gen <- MWC.initialize (V.fromList []) uniform <- uniformlyDistributedPoints gen (shrinkBoundingBox 10 [zero, Vec2 300 200]) 32 pure uniform setLineJoin LineJoinRound let dm = distances points path = tspNearestNeighbourPath dm tour = Polygon (V.toList (pathToPoints points path)) cairoScope $ do setColor (mma 0) for_ points $ \p -> sketch (Circle p 3) >> fill cairoScope $ do setColor (mma 2) sketch tour stroke :} Generated file: size 11KB, crc32: 0x1474da44
Improvement
2-opt
tsp2optPath :: Distances -> Vector TspPoint -> Vector TspPoint Source #
Move towards a local minimum using the 2-opt algorithm: delete 2 edges, and if the reconnected version is shorter than the original. The result of this algorithm is guaranteed to be at most as large as its input.
Starting this algoritm from a path optimized by tspNearestNeighbourPath
speeds
it up considerably, although this usually yields a different but similarly good
result than applying it directly.
(image code)
>>>
:{
haddockRender "Numerics/Optimization/TSP/2-opt.svg" 300 200 $ \_ -> do points <- liftIO $ do gen <- MWC.initialize (V.fromList []) uniform <- uniformlyDistributedPoints gen (shrinkBoundingBox 10 [zero, Vec2 300 200]) 32 pure uniform setLineJoin LineJoinRound let dm = distances points path = tsp2optPath dm (tspNearestNeighbourPath dm) tour = Polygon (V.toList (pathToPoints points path)) cairoScope $ do setColor (mma 0) for_ points $ \p -> sketch (Circle p 3) >> fill cairoScope $ do setColor (mma 3) sketch tour stroke :} Generated file: size 11KB, crc32: 0x73c4af54
tsp2optInplace :: PrimMonad m => Distances -> MVector (PrimState m) TspPoint -> m () Source #
Inplace version of tsp2optPath
.
:: PrimMonad m | |
=> Distances | |
-> Int | \(i \in [0\ldots n-3]\) |
-> Int | \(j \in [i+2\ldots n-1]\) |
-> MVector (PrimState m) TspPoint | \(n\) entries |
-> m Double | Length delta. Negative means the swap shortens the path. |
How much would the path length change if the two segments were swapped?
Does not modify the input vector, but marked as unsafe
because out of bounds
will error
.
For a bit more on the args’ constraints, see unsafeCommit3optSwap
.
:: PrimMonad m | |
=> Int | \(i \in [0\ldots n-3]\) |
-> Int | \(j \in [i+2\ldots n-1]\) |
-> MVector (PrimState m) a | \(n\) entries |
-> m () |
Perform a 2-opt swap as specified.
The requirements on \(i,j\) are not checked, hence unsafe
. For details see
unsafeCommit3optSwap
.
3-opt
tsp3optPath :: Distances -> Vector TspPoint -> Vector TspPoint Source #
Move towards a local minimum using the 3-opt algorithm: delete 3 edges, and if the reconnected version is shorter than the original. The result of this algorithm is guaranteed to be at most as large as its input.
3-opt yields higher quality results than 2-opt, but is considerably slower. It
is highly advised to only run it on already optimized paths, e.g. starting from
tspNearestNeighbourPath
or tsp2optPath
.
(image code)
>>>
:{
haddockRender "Numerics/Optimization/TSP/3-opt.svg" 300 200 $ \_ -> do points <- liftIO $ do gen <- MWC.initialize (V.fromList []) uniform <- uniformlyDistributedPoints gen (shrinkBoundingBox 10 [zero, Vec2 300 200]) 32 pure uniform setLineJoin LineJoinRound let dm = distances points path = tsp3optPath dm (tspNearestNeighbourPath dm) tour = Polygon (V.toList (pathToPoints points path)) cairoScope $ do setColor (mma 0) for_ points $ \p -> sketch (Circle p 3) >> fill cairoScope $ do setColor (mma 4) sketch tour stroke :} Generated file: size 11KB, crc32: 0x6c4e9493
tsp3optInplace :: PrimMonad m => Distances -> MVector (PrimState m) TspPoint -> m () Source #
Inplace version of tsp3optPath
.
Swapping cases of 3-opt. Created by unsafePlan3optSwap
, consumed by
unsafeCommit3optSwap
.
Instances
Enum Case3opt Source # | |
Defined in Numerics.Optimization.TSP | |
Show Case3opt Source # | |
Eq Case3opt Source # | |
Ord Case3opt Source # | |
Defined in Numerics.Optimization.TSP |
:: PrimMonad m | |
=> Distances | \(n\times n\) matrix |
-> Int | \(i \in [0\ldots n-5]\) |
-> Int | \(j \in [i+2\ldots n-3]\) |
-> Int | \(k \in [j+2\ldots n-1]\) |
-> MVector (PrimState m) TspPoint | \(n\) entries |
-> m (Maybe (Case3opt, Double)) | Best case, and the (negative) length delta compared to the old path. |
If an improvement can be made by 3 segments, which case yields the largest improvement, and by what amount?
Does not modify the input vector, but marked as unsafe
because out of bounds
will error
.
For a bit more on the args’ constraints, see unsafeCommit3optSwap
.
:: PrimMonad m | |
=> Case3opt | |
-> Int | \(i \in [0\ldots n-5]\) |
-> Int | \(j \in [i+2\ldots n-3]\) |
-> Int | \(k \in [j+2\ldots n-1]\) |
-> Int | \(n\) |
-> MVector (PrimState m) a | \(n\) points |
-> m () |
Perform a 3-opt swap as specified.
The requirements on \(i,j,k\) are not checked, hence unsafe
. Their purpose is
making a minimal search of the problem space possible, exploiting all symmetries
available by a symmetrical distance function and the 3-opt swaps:
- We can choose \(i<j<k\) WLOG, because 3-opt swaps are 3-symmetric.
- \(i\) needs to leave space for \(i+1,j,j+1,k,k+1\), hence the \(n-5\).
- Similarly, \(j\) needs to leave space for \(j+1,k,k+1\), hence the \(n-3\).
- \(k\) can point to the very end, leaving \(k+1\) to cycle around to the beginning.