Kotlin with JGraphT, 3009 2984 (thanks to ophact) bytes (plus header/footer)
import org.jgrapht.Graph
import org.jgrapht.GraphPath
import org.jgrapht.alg.connectivity.ConnectivityInspector
import org.jgrapht.alg.shortestpath.AllDirectedPaths
import org.jgrapht.alg.shortestpath.DijkstraManyToManyShortestPaths
import org.jgrapht.graph.DefaultDirectedGraph
import org.jgrapht.graph.DefaultEdge
import org.jgrapht.graph.DefaultUndirectedGraph
import org.jgrapht.graph.builder.GraphBuilder
import kotlin.streams.asStream
sealed class P
object C:P()
object M:P()
object E:P()
object R:P()
typealias D=Pair<Int,Int>
typealias L<T> =List<T>
typealias A<T> =Array<T>
typealias F=DefaultEdge
typealias G=BooleanArray
object H:ThreadLocal<G>(){override fun initialValue()=G(1 shl(B*2)){false}}
const val B=4
fun GraphPath<D,F>.v():A<D>{val r=arrayOfNulls<D>(edgeList.size+1)
r[0]=graph.getEdgeSource(edgeList.first())
edgeList.forEachIndexed{i,e->r[i+1]=graph.getEdgeTarget(e)}
return r as A<D>}
fun A<GraphPath<D,F>>.d()=sumOf{val table=if(B<=3){G(1 shl(B*2)){false}}else{H.get().also{for(i in it.indices){it[i]=false}}}
it.v().sumOf{val hash=(it.first+1)or(it.second+1).shl(B)
if(table[hash]){0}else{table[hash]=true
1}.toInt()}}
fun j(b:GraphBuilder<D,*,*>,x:Int,y:Int){val others=(x-1..x+1).flatMap{z->(y-1..y+1).map{z to it}}
others.forEach{b.addEdge(x to y,it)}}
fun g(g:L<L<P>>):Pair<Set<D>,Graph<D,F>>{val b=DefaultDirectedGraph.createBuilder<D,F>(::DefaultEdge)
val c=mutableSetOf<D>()
g.forEachIndexed{y,r->r.forEachIndexed{x,p->when(p){C->{c+=x to y
j(b,x,y)}
E->j(b,x,y)
R->error("")}}}
val width=g[0].size
g.indices.forEach{y->if (g[y][0]==M)j(b,-1,y)
if (g[y].last()==M)j(b,width,y)}
(0 until width).forEach{x->if (g[0][x]==M)j(b,x,-1)
if (g.last()[x]==M)j(b,x,g.size)}
return c to b.buildAsUnmodifiable()}
fun t(c: Set<D>, g:Graph<D,F>):Set<D>{val d=DijkstraManyToManyShortestPaths(g).getManyToManyPaths(c,c)
val h=c.flatMap{o->c.map{o to it}}.filter{if(it.first.first==it.second.first)it.first.second>it.second.second else it.first.first>it.second.first}.associateWith{d.getPath(it.first,it.second)}.map{it.key to AllDirectedPaths(g).getAllPaths(it.key.first,it.key.second,true,it.value.length)}
val j=h.map{it.first}
val k=h.map{it.second}
val l=k.s{val m=DefaultUndirectedGraph.createBuilder<D,F>(::DefaultEdge)
m.addVertices(*c.toTypedArray())
it.mapIndexed{i,b->if(b)j[i]else null}.filterNotNull().forEach{m.addEdge(it.first,it.second)}
ConnectivityInspector(m.buildAsUnmodifiable()).isConnected}
return HashSet(l.asStream().parallel().map{it to it.d()}.min{n,o->n.second-o.second}.get().first.flatMap{it.v().asList()})}
fun a(a:String)=a.lines().map{it.map{when(it){'M'->M
'.',' '->E
'C'->C
'R'->R
else->null}}.filterNotNull()}.filter{it.isNotEmpty()}
fun l(data:L<L<P>>, route:Set<D>)=route.sumOf{if(it.second<0||it.second>data.lastIndex)return@sumOf 1
val row=data[it.second]
if(it.first<0||it.first>row.lastIndex)return@sumOf 1
if(row[it.first]==E)1.toInt()else 0}
inline fun<reified T>L<L<T>>.s(noinline test:(A<Boolean>)->Boolean)=List(size){listOf(false,true)}.c().filter(test).let{sequence{for(selection in it){yieldAll(filterIndexed { i, _->selection[i]}.c())}}}
inline fun <reified T>L<L<T>>.c()=sequence{if(isEmpty())return@sequence
val j=map{it.iterator()}.toTypedArray()
val v=j.map{it.next()}.toTypedArray()
yield(v.clone())
while(true){var i=0
while(!j[i].hasNext()){j[i]=get(i).iterator()
v[i]=j[i].next()
if(++i>lastIndex)return@sequence}
v[i]=j[i].next()
yield(v.clone())}}
fun main() {
val art = System.`in`.bufferedReader().readText()
val data = a(art)
val (cities, graph) = g(data)
val route = t(cities, graph)
println("Length: ${l(data, route)}")
}
Here is the original, prior to golfing:
import org.jgrapht.Graph
import org.jgrapht.GraphPath
import org.jgrapht.alg.connectivity.ConnectivityInspector
import org.jgrapht.alg.shortestpath.AllDirectedPaths
import org.jgrapht.alg.shortestpath.DijkstraManyToManyShortestPaths
import org.jgrapht.graph.DefaultDirectedGraph
import org.jgrapht.graph.DefaultEdge
import org.jgrapht.graph.DefaultUndirectedGraph
import org.jgrapht.graph.builder.GraphBuilder
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.thread
import kotlin.streams.asStream
sealed class Place
object City : Place()
object Mountain : Place()
object Empty : Place()
object Road : Place()
typealias Coord = Pair<Int, Int>
operator fun Coord.compareTo(other: Coord): Int {
return first.compareTo(other.first).let {
if (it == 0)
second.compareTo(other.second)
else
it
}
}
fun <T> Collection<T>.toHashSet() = HashSet(this)
object HashTable : ThreadLocal<BooleanArray>() {
override fun initialValue(): BooleanArray {
return BooleanArray(1 shl (MAX_DIMEN_BITS * 2)) { false }
}
}
const val MAX_DIMEN_BITS = 4
inline fun <reified V, E> GraphPath<V, E>.getVertexListFast(): Array<V> {
val ret = Array<V?>(edgeList.size + 1) { null }
ret[0] = graph.getEdgeSource(edgeList.first())
edgeList.forEachIndexed { i, edge ->
ret[i + 1] = graph.getEdgeTarget(edge)
}
@Suppress("UNCHECKED_CAST")
return ret as Array<V>
}
fun Array<GraphPath<Coord, DefaultEdge>>.distinctSize() = sumOf { path ->
val table = if (MAX_DIMEN_BITS <= 3) {
BooleanArray(1 shl (MAX_DIMEN_BITS * 2)) { false }
} else {
HashTable.get().also {
for (i in it.indices) {
it[i] = false
}
}
}
path.getVertexListFast().sumOf {
val hash = (it.first + 1) or (it.second + 1).shl(MAX_DIMEN_BITS)
if (table[hash]) {
0
} else {
table[hash] = true
1
}.toInt() /* stupid overload resolution */
}
}
fun joinUp(builder: GraphBuilder<Coord, *, *>, x: Int, y: Int) {
val others = (x - 1 .. x + 1).flatMap { otherX -> (y - 1 .. y + 1).map { otherY -> otherX to otherY } }
others.forEach {
builder.addEdge(x to y, it)
}
}
fun createGraph(grid: List<List<Place>>): Pair<Set<Coord>, Graph<Coord, DefaultEdge>> {
val builder = DefaultDirectedGraph.createBuilder<Coord, DefaultEdge> { DefaultEdge() }
val cities = mutableSetOf<Coord>()
grid.forEachIndexed { y, row ->
row.forEachIndexed { x, place ->
when (place) {
City -> {
cities += x to y
joinUp(builder, x, y)
}
Mountain -> {
}
Empty -> joinUp(builder, x, y)
Road -> error("Cannot pre-supply Roads")
}
}
}
val width = grid.first().size
grid.indices.forEach { y ->
if (grid[y].first() == Mountain)
joinUp(builder, -1, y)
if (grid[y].last() == Mountain)
joinUp(builder, width, y)
}
(0 until width).forEach { x ->
if (grid.first()[x] == Mountain)
joinUp(builder, x, -1)
if (grid.last()[x] == Mountain)
joinUp(builder, x, grid.size)
}
return cities to builder.buildAsUnmodifiable()
}
fun joinCities(cities: Set<Coord>, graph: Graph<Coord, DefaultEdge>): Set<Coord> {
val allPathsAlgo = AllDirectedPaths(graph)
val dijkstraAlgo = DijkstraManyToManyShortestPaths(graph)
val shortestPaths = dijkstraAlgo.getManyToManyPaths(cities, cities)
val sourceTargets = cities.flatMap { outer -> cities.map { outer to it } }.filter { it.first > it.second }
val shortestLengths = sourceTargets.associateWith {
shortestPaths.getPath(it.first, it.second)
}
val allShortestPathsWithKey = shortestLengths.map {
it.key to allPathsAlgo.getAllPaths(it.key.first, it.key.second, true, it.value.length)
}
val allShortestPathsKeys = allShortestPathsWithKey.map { it.first }
val allShortestPaths = allShortestPathsWithKey.map { it.second }
val i = AtomicLong()
val (total, allPathsProduct) = allShortestPaths.selectiveCartesianProduct { selection ->
val builder = DefaultUndirectedGraph.createBuilder<Coord, DefaultEdge> { DefaultEdge() }
builder.addVertices(*cities.toTypedArray())
selection.mapIndexed { i, bool -> if (bool) allShortestPathsKeys[i] else null }.filterNotNull().forEach { builder.addEdge(it.first, it.second) }
val selectionGraph = builder.buildAsUnmodifiable()
ConnectivityInspector(selectionGraph).isConnected
}
val startTime = System.currentTimeMillis()
val counterThread = thread(true) {
try {
while (true) {
Thread.sleep(1000)
val progress = i.getOpaque()
val percent = (progress.toDouble() / total.toDouble()) * 100f
print("Progress: $progress\t/ $total\t($percent%)\t(${System.currentTimeMillis() - startTime} ms)\r")
}
} catch (e: InterruptedException) {
println()
}
}
val ret = allPathsProduct
.asStream()
.parallel()
.map { (it to it.distinctSize()).also { i.incrementAndGet() } }
.min { left, right -> left.second - right.second }.get().first.flatMap { it.getVertexListFast().asIterable() }.toHashSet()
counterThread.interrupt()
counterThread.join()
return ret
}
fun artToData(art: String) = art.lines().map {
it.map { char ->
when (char) {
'M' -> Mountain
'.', ' ' -> Empty
'C' -> City
'R' -> Road
else -> null
}
}.filterNotNull()
}.filter { it.isNotEmpty() }
fun plotRoute(data: List<List<Place>>, route: Set<Coord>): List<List<Place>> {
val width = data[0].size
val emptyLine = List<Place>(width + 2) { Empty }
val newData = mutableListOf(emptyLine.toMutableList(), *data.map { mutableListOf(Empty, *it.toTypedArray(), Empty) }.toTypedArray(), emptyLine.toMutableList())
for (point in route) {
if (newData[point.second + 1][point.first + 1] == Empty) {
newData[point.second + 1][point.first + 1] = Road
}
}
return newData
}
fun routeLength(data: List<List<Place>>, route: Set<Coord>): Int = route.sumOf { point ->
if (point.second < 0 || point.second > data.lastIndex) return@sumOf 1
val row = data[point.second]
if (point.first < 0 || point.first > row.lastIndex) return@sumOf 1
if (row[point.first] == Empty) 1.toInt() /* needed due to overload ambiguity */ else 0
}
fun dataToArt(data: List<List<Place>>) = buildString {
data.forEach { row ->
row.forEach { place ->
append(
when (place) {
City -> 'C'
Empty -> '.'
Mountain -> 'M'
Road -> 'R'
}
)
}
append('\n')
}
}
inline fun <reified T> List<Collection<T>>.selectiveCartesianProduct(noinline test: (Array<Boolean>) -> Boolean): Pair<Long, Sequence<Array<T>>> {
val selections = List(size) { listOf(false, true) }.cartesianProduct().filter(test)
val total = selections.sumOf { selection ->
val selected = filterIndexed { i, _ -> selection[i] }
if (selected.isEmpty()) 0 else selected.fold(1L) { acc, value -> acc * value.size }
}
return total to sequence {
for (selection in selections) {
yieldAll(filterIndexed { i, _ -> selection[i] }.cartesianProduct())
}
}
}
inline fun <reified T> List<Iterable<T>>.cartesianProduct() = sequence {
if (isEmpty()) return@sequence
val iterators = map { it.iterator() }.toTypedArray()
val values = iterators.map { it.next() }.toTypedArray()
yield(values.clone())
while (true) {
var i = 0
while (!iterators[i].hasNext()) {
// reset current iterator, move on to check next one
iterators[i] = get(i).iterator()
values[i] = iterators[i].next()
if (++i > lastIndex)
return@sequence
}
values[i] = iterators[i].next()
yield(values.clone())
}
}
fun main() {
val art = System.`in`.bufferedReader().readText()
val data = artToData(art)
val (cities, graph) = createGraph(data)
val route = joinCities(cities, graph)
println("Length: ${routeLength(data, route)}")
val newData = plotRoute(data, route)
println(dataToArt(newData))
}
The algorithm is very intractable: with the sample with 6 cities, it takes ~300 minutes of CPU time on my high-end desktop
Note that if your input data is longer than 14 on any dimension, you have to increase MAX_DIMEN_BITS.
Input is sent to stdin followed by EOF (^D) as ASCII art.
...../.MCM./.C.C./.MCM./.....
\$\endgroup\$.RRRRRCRRRR./RMMMMMMMMMMR/.C........C.
\$\endgroup\$CC
0? Or is it 1? Or is it something else? \$\endgroup\$