Skip to content

Commit 92a0074

Browse files
committed
Solution 2015-09 (All in a Single Night)
1 parent 8c97bb0 commit 92a0074

File tree

5 files changed

+286
-24
lines changed

5 files changed

+286
-24
lines changed

src/main/kotlin/de/ronny_h/aoc/AdventOfCode.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ abstract class AdventOfCode<T>(val year: Int, val day: Int) {
2222
?: error("The input file for day $day, year $year could not be found. Expected at '$inputFile' in the resources folder.")
2323
}
2424

25-
private fun paddedDay(): String = day.toString().padStart(2, '0')
25+
fun paddedDay(): String = day.toString().padStart(2, '0')
2626

2727
private fun printAndCheck(input: List<String>, block: (List<String>) -> T, expected: T) {
2828
printAndCheck(measureTimedValue { block(input) }, expected)

src/main/kotlin/de/ronny_h/aoc/extensions/graphs/TravelingSalesman.kt

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package de.ronny_h.aoc.extensions.graphs
22

33
import java.util.*
4+
import javax.annotation.processing.Generated
5+
import kotlin.math.ceil
46

57

68
data 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+
*/
919
class 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
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package de.ronny_h.aoc.year2015.day09
2+
3+
import de.ronny_h.aoc.AdventOfCode
4+
import de.ronny_h.aoc.extensions.graphs.Edge
5+
import de.ronny_h.aoc.extensions.graphs.TravelingSalesman
6+
import de.ronny_h.aoc.extensions.graphs.TravelingSalesmanProblemSolution
7+
import de.ronny_h.aoc.extensions.graphs.toAdjacencyMatrix
8+
import java.io.File
9+
10+
fun main() = AllInASingleNight().run(251, 898)
11+
12+
class AllInASingleNight : AdventOfCode<Int>(2015, 9) {
13+
override fun part1(input: List<String>) = input.parseEdges().shortestRoundtrip().length
14+
15+
companion object {
16+
fun List<String>.parseEdges() = this.map { line ->
17+
val (locations, distance) = line.split(" = ")
18+
val (from, to) = locations.split(" to ")
19+
Edge(from, to, distance.toInt())
20+
}
21+
}
22+
23+
override fun part2(input: List<String>): Int {
24+
val distances = input.parseEdges()
25+
val maxWeight = distances.maxOf { it.weight } + 1
26+
val invertedDistances = distances.map { it.copy(weight = maxWeight - it.weight) }
27+
val shortestRoundtrip = invertedDistances.shortestRoundtrip()
28+
return (shortestRoundtrip.path.size - 3) * maxWeight - shortestRoundtrip.length
29+
}
30+
31+
private fun List<Edge<String>>.shortestRoundtrip(): TravelingSalesmanProblemSolution {
32+
val (edges, adjacencyMatrix) = toAdjacencyMatrix(
33+
symmetrical = true,
34+
nullNode = "SyntheticStart"
35+
)
36+
val shortestRoundtrip = TravelingSalesman(adjacencyMatrix.content).calculateShortestRoundTrip()
37+
println("nodes: ${adjacencyMatrix.headers}")
38+
println("path: ${shortestRoundtrip.path}")
39+
// writeToDotFile("part2", edges, adjacencyMatrix.headers, shortestRoundtrip.path)
40+
return shortestRoundtrip
41+
}
42+
43+
private fun writeToDotFile(name: String, distances: List<Edge<String>>, headers: List<String>, path: List<Int>) {
44+
val indexOf = headers.mapIndexed { i, name -> name to i }.toMap()
45+
46+
File("${sourcePath()}/$name.dot").printWriter().use { out ->
47+
out.println("strict graph {")
48+
out.println(" edge [fontname=Arial fontsize=9]")
49+
out.println(" node [fontname=Arial]")
50+
51+
headers.forEachIndexed { i, name ->
52+
out.println(" $i [label=\"$i $name\"]")
53+
}
54+
distances.forEach {
55+
val color = if (it.weight == 0) " color=grey48 fontcolor=grey48" else ""
56+
out.println(" ${indexOf[it.from]} -- ${indexOf[it.to]} [label=${it.weight}$color]")
57+
}
58+
path.windowed(2) { (from, to) ->
59+
out.println(" $from -- $to [color=red]")
60+
}
61+
62+
out.println("}")
63+
}
64+
}
65+
66+
private fun sourcePath(): String = "src/main/kotlin/de/ronny_h/aoc/year$year/day${paddedDay()}"
67+
}

src/test/kotlin/de/ronny_h/aoc/extensions/graphs/TravelingSalesmanTest.kt

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,75 @@ import io.kotest.matchers.shouldBe
55

66
class TravelingSalesmanTest : StringSpec({
77

8-
"TSP for a graph with 4 nodes given as adjacency matrix" {
8+
"TSP for a complete round-trip on a graph with 4 nodes" {
99
val adj: Array<IntArray> = arrayOf(
1010
intArrayOf(0, 10, 15, 20),
1111
intArrayOf(10, 0, 35, 25),
1212
intArrayOf(15, 35, 0, 30),
1313
intArrayOf(20, 25, 30, 0)
1414
)
1515

16-
val shortestPath = TravelingSalesman(adj).tsp()
16+
val shortestPath = TravelingSalesman(adj).calculateShortestRoundTrip()
1717

1818
shortestPath.length shouldBe 80
1919
shortestPath.path shouldBe listOf(0, 1, 3, 2, 0)
2020
}
2121

22+
"convert a map of a single distance to an adjacency matrix" {
23+
val distances = listOf(Edge("A", "B", 1))
24+
val expectedAdjacencyMatrix = arrayOf(
25+
intArrayOf(0, 1),
26+
intArrayOf(MAX_WEIGHT, 0),
27+
)
28+
distances.toAdjacencyMatrix() shouldBe (distances to AdjacencyMatrix(listOf("A", "B"), expectedAdjacencyMatrix))
29+
}
30+
31+
"convert a map of multiple distances to an adjacency matrix" {
32+
val distances = listOf(Edge("A", "B", 1), Edge("B", "C", 2))
33+
val expectedAdjacencyMatrix = arrayOf(
34+
intArrayOf(0, 1, MAX_WEIGHT),
35+
intArrayOf(MAX_WEIGHT, 0, 2),
36+
intArrayOf(MAX_WEIGHT, MAX_WEIGHT, 0),
37+
)
38+
distances.toAdjacencyMatrix() shouldBe (distances to AdjacencyMatrix(
39+
listOf("A", "B", "C"),
40+
expectedAdjacencyMatrix
41+
))
42+
}
43+
44+
"convert a map of multiple distances to an adjacency matrix symmetrically" {
45+
val distances = listOf(Edge("A", "B", 1), Edge("B", "C", 2))
46+
val expectedAdjacencyMatrix = arrayOf(
47+
intArrayOf(0, 1, MAX_WEIGHT),
48+
intArrayOf(1, 0, 2),
49+
intArrayOf(MAX_WEIGHT, 2, 0),
50+
)
51+
distances.toAdjacencyMatrix(symmetrical = true) shouldBe (distances to AdjacencyMatrix(
52+
listOf("A", "B", "C"),
53+
expectedAdjacencyMatrix
54+
))
55+
}
56+
57+
"when providing a nullNode, synthetic edges of length 0 are added" {
58+
val distances = listOf(Edge("A", "B", 1), Edge("B", "C", 2))
59+
60+
val syntheticEdges = listOf(
61+
Edge("ThisNodeIsArtificial", "A", 0),
62+
Edge("ThisNodeIsArtificial", "B", 0),
63+
Edge("ThisNodeIsArtificial", "C", 0),
64+
)
65+
val expectedAdjacencyMatrix = arrayOf(
66+
intArrayOf(0, 0, 0, 0),
67+
intArrayOf(0, 0, 1, MAX_WEIGHT),
68+
intArrayOf(0, 1, 0, 2),
69+
intArrayOf(0, MAX_WEIGHT, 2, 0),
70+
)
71+
distances.toAdjacencyMatrix(
72+
symmetrical = true,
73+
"ThisNodeIsArtificial"
74+
) shouldBe (syntheticEdges + distances to AdjacencyMatrix(
75+
listOf("ThisNodeIsArtificial", "A", "B", "C"),
76+
expectedAdjacencyMatrix
77+
))
78+
}
2279
})

0 commit comments

Comments
 (0)