Skip to content

Commit 8c97bb0

Browse files
committed
A Traveling Salesman Problem solution using Branch And Bound
* The implementation is inspired from the java implementation on https://www.geeksforgeeks.org/dsa/traveling-salesman-problem-using-branch-and-bound-2/ * It is far from being ideomatic Kotlin and refactored just a little bit, being a base for further generalization.
1 parent 339e3c8 commit 8c97bb0

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package de.ronny_h.aoc.extensions.graphs
2+
3+
import java.util.*
4+
5+
6+
data class TravelingSalesmanProblemSolution(val length: Int, val path: List<Int>)
7+
8+
// TSP implementation inspired from https://www.geeksforgeeks.org/dsa/traveling-salesman-problem-using-branch-and-bound-2/
9+
class TravelingSalesman(private val adj: Array<IntArray>) {
10+
private val N: Int = adj.size
11+
12+
// finalPath[] stores the final solution ie, the path of the salesman.
13+
private val finalPath: IntArray = IntArray(N + 1)
14+
15+
// visited[] keeps track of the already visited nodes in a particular path
16+
private val visited: BooleanArray = BooleanArray(N)
17+
18+
// Stores the final minimum weight of shortest tour.
19+
private var finalLength: Int = Int.Companion.MAX_VALUE
20+
21+
// Function to copy temporary solution to the final solution
22+
private fun copyToFinal(currentPath: IntArray) {
23+
for (i in 0..<N) {
24+
finalPath[i] = currentPath[i]
25+
}
26+
finalPath[N] = currentPath[0]
27+
}
28+
29+
// Function to find the minimum edge cost having an end at the vertex i
30+
private fun firstMin(i: Int): Int {
31+
var min = Int.MAX_VALUE
32+
for (k in 0..<N) {
33+
if (adj[i][k] < min && i != k) min = adj[i][k]
34+
}
35+
return min
36+
}
37+
38+
// function to find the second minimum edge cost having an end at the vertex i
39+
private fun secondMin(i: Int): Int {
40+
var first = Int.MAX_VALUE
41+
var second = Int.MAX_VALUE
42+
for (j in 0..<N) {
43+
if (i == j) continue
44+
45+
if (adj[i][j] <= first) {
46+
second = first
47+
first = adj[i][j]
48+
} else if (adj[i][j] <= second && adj[i][j] != first) {
49+
second = adj[i][j]
50+
}
51+
}
52+
return second
53+
}
54+
55+
/** function that takes as arguments:
56+
* @param boundSoFar lower bound of the root node
57+
* @param weightSoFar stores the weight of the path so far
58+
* @param level current level while moving in the search space tree
59+
* @param currentPath where the solution is being stored which would later be copied to [finalPath]
60+
*/
61+
private fun tspRecursive(boundSoFar: Int, weightSoFar: Int, level: Int, currentPath: IntArray) {
62+
var currentBound = boundSoFar
63+
var currentWeight = weightSoFar
64+
65+
// base case is when we have reached level N which
66+
// means we have covered all the nodes once
67+
if (level == N) {
68+
// 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) {
70+
// currentLength has the total weight of the solution we got
71+
val currentLength = currentWeight + adj[currentPath[level - 1]][currentPath[0]]
72+
73+
// Update final result and final path if current result is better.
74+
if (currentLength < finalLength) {
75+
copyToFinal(currentPath)
76+
finalLength = currentLength
77+
}
78+
}
79+
return
80+
}
81+
82+
// for any other level iterate for all vertices to build the search space tree recursively
83+
for (i in 0..<N) {
84+
// 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]) {
87+
val temp = currentBound
88+
currentWeight += adj[currentPath[level - 1]][i]
89+
90+
// different computation of curr_bound for level 2 from the other levels
91+
currentBound -= if (level == 1) {
92+
(firstMin(currentPath[0]) + firstMin(i)) / 2
93+
} else {
94+
(secondMin(currentPath[level - 1]) + firstMin(i)) / 2
95+
}
96+
97+
// currentBound + currentWeight is the actual lower bound for the node that we have arrived on
98+
// If current lower bound < finalLength, we need to explore the node further
99+
if (currentBound + currentWeight < finalLength) {
100+
currentPath[level] = i
101+
visited[i] = true
102+
103+
// call for the next level
104+
tspRecursive(currentBound, currentWeight, level + 1, currentPath)
105+
}
106+
107+
// Else we have to prune the node by resetting
108+
// all changes to curr_weight and curr_bound
109+
currentWeight -= adj[currentPath[level - 1]][i]
110+
currentBound = temp
111+
112+
// Also reset the visited array
113+
Arrays.fill(visited, false)
114+
for (j in 0..level - 1) {
115+
visited[currentPath[j]] = true
116+
}
117+
}
118+
}
119+
}
120+
121+
// This function sets up final_path[]
122+
fun tsp(): TravelingSalesmanProblemSolution {
123+
val currentPath = IntArray(N + 1)
124+
125+
// Calculate initial lower bound for the root node
126+
// using the formula 1/2 * (sum of first min + second min) for all edges.
127+
// Also initialize the currentPath and visited array
128+
var currentBound = 0
129+
Arrays.fill(currentPath, -1)
130+
Arrays.fill(visited, false)
131+
132+
// Compute initial bound
133+
for (i in 0..<N) {
134+
currentBound += (firstMin(i) + secondMin(i))
135+
}
136+
137+
// Rounding off the lower bound to an integer
138+
currentBound = if (currentBound == 1) 1 else currentBound / 2
139+
140+
// We start at vertex 1 so the first vertex in currentPath[] is 0
141+
visited[0] = true
142+
currentPath[0] = 0
143+
144+
tspRecursive(currentBound, 0, 1, currentPath)
145+
146+
return TravelingSalesmanProblemSolution(finalLength, finalPath.toList())
147+
}
148+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package de.ronny_h.aoc.extensions.graphs
2+
3+
import io.kotest.core.spec.style.StringSpec
4+
import io.kotest.matchers.shouldBe
5+
6+
class TravelingSalesmanTest : StringSpec({
7+
8+
"TSP for a graph with 4 nodes given as adjacency matrix" {
9+
val adj: Array<IntArray> = arrayOf(
10+
intArrayOf(0, 10, 15, 20),
11+
intArrayOf(10, 0, 35, 25),
12+
intArrayOf(15, 35, 0, 30),
13+
intArrayOf(20, 25, 30, 0)
14+
)
15+
16+
val shortestPath = TravelingSalesman(adj).tsp()
17+
18+
shortestPath.length shouldBe 80
19+
shortestPath.path shouldBe listOf(0, 1, 3, 2, 0)
20+
}
21+
22+
})

0 commit comments

Comments
 (0)