11package de.ronny_h.aoc.extensions.graphs
22
33import java.util.*
4+ import javax.annotation.processing.Generated
5+ import kotlin.math.ceil
46
57
68data class TravelingSalesmanProblemSolution (val length : Int , val path : List <Int >)
79
8- // TSP implementation inspired from https://www.geeksforgeeks.org/dsa/traveling-salesman-problem-using-branch-and-bound-2/
10+ // should be larger than any other single weight in the graph
11+ const val MAX_WEIGHT = 10_000_000
12+ private const val UNSET = - 1
13+
14+ /* *
15+ * A TSP implementation inspired from https://www.geeksforgeeks.org/dsa/traveling-salesman-problem-using-branch-and-bound-2/
16+ *
17+ * @param adj An adjacency matrix containing all edge weights.
18+ */
919class TravelingSalesman (private val adj : Array <IntArray >) {
1020 private val N : Int = adj.size
1121
@@ -16,7 +26,7 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
1626 private val visited: BooleanArray = BooleanArray (N )
1727
1828 // Stores the final minimum weight of shortest tour.
19- private var finalLength: Int = Int .Companion . MAX_VALUE
29+ private var finalLength: Int = Int .MAX_VALUE
2030
2131 // Function to copy temporary solution to the final solution
2232 private fun copyToFinal (currentPath : IntArray ) {
@@ -28,7 +38,7 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
2838
2939 // Function to find the minimum edge cost having an end at the vertex i
3040 private fun firstMin(i: Int ): Int {
31- var min = Int . MAX_VALUE
41+ var min = MAX_WEIGHT
3242 for (k in 0.. <N ) {
3343 if (adj[i][k] < min && i != k) min = adj[i][k]
3444 }
@@ -37,8 +47,8 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
3747
3848 // function to find the second minimum edge cost having an end at the vertex i
3949 private fun secondMin(i: Int ): Int {
40- var first = Int . MAX_VALUE
41- var second = Int . MAX_VALUE
50+ var first = MAX_WEIGHT
51+ var second = MAX_WEIGHT
4252 for (j in 0.. <N ) {
4353 if (i == j) continue
4454
@@ -52,7 +62,9 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
5262 return second
5363 }
5464
55- / ** function that takes as arguments:
65+ / **
66+ * Calculates the shortest tour recursively.
67+ *
5668 * @param boundSoFar lower bound of the root node
5769 * @param weightSoFar stores the weight of the path so far
5870 * @param level current level while moving in the search space tree
@@ -66,7 +78,7 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
6678 // means we have covered all the nodes once
6779 if (level == N ) {
6880 // check if there is an edge from last vertex in path back to the first vertex
69- if (adj[currentPath[level - 1]][currentPath[0]] != 0 ) {
81+ if (adj[currentPath[level - 1]][currentPath[0]] != MAX_WEIGHT ) {
7082 // currentLength has the total weight of the solution we got
7183 val currentLength = currentWeight + adj[currentPath[level - 1]][currentPath[0]]
7284
@@ -82,16 +94,16 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
8294 // for any other level iterate for all vertices to build the search space tree recursively
8395 for (i in 0.. <N ) {
8496 // Consider next vertex if it is not same (diagonal
85- // entry in adjacency matrix and not visited already)
86- if (adj[ currentPath[level - 1]][i] != 0 && ! visited[i]) {
97+ // entry in adjacency matrix) and not visited already
98+ if (i != currentPath[level - 1] && ! visited[i]) {
8799 val temp = currentBound
88100 currentWeight + = adj[currentPath[level - 1]][i]
89101
90- // different computation of curr_bound for level 2 from the other levels
102+ // different computation of currentBound for level 2 from the other levels
91103 currentBound - = if (level == 1) {
92- ( firstMin(currentPath[0]) + firstMin(i)) / 2
104+ ceil(( firstMin(currentPath[0]) + firstMin(i)) / 2.0).toInt()
93105 } else {
94- ( secondMin(currentPath[level - 1]) + firstMin(i)) / 2
106+ ceil(( secondMin(currentPath[level - 1]) + firstMin(i)) / 2.0).toInt()
95107 }
96108
97109 // currentBound + currentWeight is the actual lower bound for the node that we have arrived on
@@ -105,28 +117,32 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
105117 }
106118
107119 // Else we have to prune the node by resetting
108- // all changes to curr_weight and curr_bound
120+ // all changes to currentWeight and currentBound
109121 currentWeight - = adj[currentPath[level - 1]][i]
110122 currentBound = temp
111123
112124 // Also reset the visited array
113125 Arrays .fill(visited, false)
114- for (j in 0.. level - 1) {
115- visited[currentPath[j]] = true
126+ for (j in 0.. <level) {
127+ if (currentPath[j] != UNSET ) {
128+ visited[currentPath[j]] = true
129+ }
116130 }
117131 }
118132 }
119133 }
120134
121- // This function sets up final_path[]
122- fun tsp(): TravelingSalesmanProblemSolution {
123- val currentPath = IntArray (N + 1)
135+ fun calculateShortestRoundTrip(): TravelingSalesmanProblemSolution {
136+ tsp()
137+ return TravelingSalesmanProblemSolution (finalLength, finalPath.toList())
138+ }
124139
140+ private fun tsp() {
125141 // Calculate initial lower bound for the root node
126142 // using the formula 1/ 2 * (sum of first min + second min) for all edges.
127143 // Also initialize the currentPath and visited array
128144 var currentBound = 0
129- Arrays .fill( currentPath, - 1)
145+ val currentPath = IntArray ( N + 1) { UNSET }
130146 Arrays .fill(visited, false)
131147
132148 // Compute initial bound
@@ -135,14 +151,80 @@ class TravelingSalesman(private val adj: Array<IntArray>) {
135151 }
136152
137153 // Rounding off the lower bound to an integer
138- currentBound = if (currentBound == 1) 1 else currentBound / 2
154+ currentBound = ceil (currentBound / 2.0).toInt()
139155
140156 // We start at vertex 1 so the first vertex in currentPath[] is 0
141157 visited[0] = true
142158 currentPath[0] = 0
143159
144160 tspRecursive(currentBound, 0, 1, currentPath)
161+ }
162+ }
145163
146- return TravelingSalesmanProblemSolution (finalLength, finalPath.toList())
164+ data class Edge <N >(val from: N , val to: N , val weight: Int )
165+ data class AdjacencyMatrix <N >(val headers : List <N >, val content : Array <IntArray >) {
166+ init {
167+ require(headers.size == content.size) { " adjacency matrix's headers (${headers.size} ) do not match its content (${content.size} )" }
168+ require(content.all { it.size == headers.size }) { " adjacency matrix is not quadratical" }
169+ }
170+
171+ @Generated
172+ override fun equals (other : Any? ): Boolean {
173+ if (this == = other) return true
174+ if (javaClass != other?.javaClass) return false
175+
176+ other as AdjacencyMatrix <* >
177+
178+ if (headers != other.headers) return false
179+ if (! content.contentDeepEquals(other.content)) return false
180+
181+ return true
182+ }
183+
184+ @Generated
185+ override fun hashCode (): Int {
186+ var result = headers.hashCode()
187+ result = 31 * result + content.contentDeepHashCode()
188+ return result
189+ }
190+ }
191+
192+ /* *
193+ * Converts a list of [Edge]s to an adjacency matrix.
194+ *
195+ * @param symmetrical If `true`, treats all connections in the list of edges symmetrically. That means, an edge from _a_ to _b_ with weight _w_
196+ * implies an edge from _b_ to _a_ with weight _w_. Else (the default), only edges that are explicitly in the list are added to the matrix.
197+ * @param nullNode If specified (i.e. not `null`), adds a synthetic start node to the matrix with edges of weight `0` to all other nodes.
198+ * This way, instead of a complete round-trip, the TSP algorithm calculates a single path containing all nodes - starting with an arbitrary one -
199+ * so that the path length is minimized.
200+ */
201+ fun <N > List<Edge<N>>.toAdjacencyMatrix (
202+ symmetrical : Boolean = false,
203+ nullNode : N ? = null
204+ ): Pair <List <Edge <N >>, AdjacencyMatrix<N>> {
205+ val originalNodes = flatMap { listOf (it.from, it.to) }.toSet().toList()
206+
207+ // to solve the start node problem, add a "null node" with distance 0 to all others
208+ val nullNodes = nullNode?.let { listOf (it) } ? : emptyList()
209+ val nullEdges = nullNode?.let { n -> originalNodes.map { o -> Edge (n, o, 0 ) } } ? : emptyList()
210+
211+ val nodes = nullNodes + originalNodes
212+ val indices = nodes.mapIndexed { i, node -> node to i }.toMap()
213+ val adj = Array (nodes.size) { i ->
214+ IntArray (nodes.size) { j -> if (i == j) 0 else MAX_WEIGHT }
215+ }
216+
217+ val allEdges = nullEdges + this
218+ allEdges.forEach {
219+ val fromIndex = indices.getValue(it.from)
220+ val toIndex = indices.getValue(it.to)
221+ check(adj[fromIndex][toIndex] == MAX_WEIGHT ) { " there is already an edge from node $fromIndex (${it.from} ) to $toIndex (${it.to} )" }
222+ check(it.weight < MAX_WEIGHT ) { " this algorithm only works for weights smaller than $MAX_WEIGHT , current: ${it.weight} " }
223+ adj[fromIndex][toIndex] = it.weight
224+ if (symmetrical) {
225+ check(adj[toIndex][fromIndex] == MAX_WEIGHT ) { " there is already an edge from node $toIndex (${it.to} ) to $fromIndex (${it.from} )" }
226+ adj[toIndex][fromIndex] = it.weight
227+ }
147228 }
229+ return allEdges to AdjacencyMatrix (nodes, adj)
148230}
0 commit comments