August 27, 2024

P91 - Knight's tour.

This is another classical problem in computer science. How can a knight jump on an N×N chessboard in such a way that it visits every square exactly once?

Write a function knightsTours(N, (X, Y)) to list all knight tours that be made from (X, Y) on a N×N chessboard. Hints: It might help to represent squares by pairs of their coordinates of the form Pair(X, Y), where X and Y are integers between 0 and N-1. Alternatively, define a Point data class for this purpose.

Can you find only "closed tours", where the knight can jump from its final position back to its starting position? Can you make a lazy list that only calculates the tours as needed?

kotlin

package org.kotlin99.misc

import com.natpryce.hamkrest.anyElement
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import org.junit.Test
import org.kotlin99.common.containsAll

fun knightsToursLazy(boardSize: Int, point: Point, tour: Tour = Tour(point)): Sequence<Tour> {
    if (tour.size == boardSize * boardSize) return sequenceOf(tour)
    return point.knightMoves(boardSize).asSequence()
        .filterNot { tour.contains(it) }
        //.sortedBy { it.knightMoves(boardSize).filterNot{ tour.contains(it) }.size } // https://en.wikipedia.org/wiki/Knight%27s_tour#Warnsdorf.27s_rule
        .flatMap { knightsToursLazy(boardSize, it, tour + it) }
}

fun knightsTours(boardSize: Int, point: Point, tour: Tour = Tour(point)): List<Tour> {
    if (tour.size == boardSize * boardSize) return listOf(tour)
    return point.knightMoves(boardSize)
        .filterNot { tour.contains(it) }
        // .sortedBy { it.knightMoves(boardSize).filterNot{ tour.contains(it) }.size } // https://en.wikipedia.org/wiki/Knight%27s_tour#Warnsdorf.27s_rule
        .flatMap { knightsTours(boardSize, it, tour + it) }
}

data class Point(private val x: Int, private val y: Int) {
    fun knightMoves(boardSize: Int): List<Point> =
        allShifts.map { this + it }.filter { it.x in (0 until boardSize) && it.y in (0 until boardSize) }

    operator fun plus(other: Point): Point = Point(x + other.x, y + other.y)
    operator fun unaryMinus(): Point = Point(-x, -y)
    override fun toString() = "($x,$y)"

    companion object {
        private val shifts = listOf(
            Point(2, -1),
            Point(-2, -1),
            Point(1, -2),
            Point(-1, -2)
        )
        private val allShifts = shifts + shifts.map { -it }
    }
}

data class Tour(val points: Collection<Point>) {
    constructor (vararg points: Point): this(points.asList())

    val size: Int get() = points.size
    fun contains(point: Point) = points.contains(point)
    infix operator fun plus(point: Point) = Tour(points + point)
}

fun Tour.isClosed(boardSize: Int): Boolean {
    return if (size <= 1) false
    else points.last().knightMoves(boardSize).contains(points.first())
}


class P91Test {
    @Test fun `knight's tours`() {
        assertThat(knightsTours(1, Point(0, 0)), equalTo(listOf(Tour(Point(0, 0)))))
        assertThat(knightsTours(2, Point(0, 0)), equalTo(emptyList()))
        assertThat(knightsTours(3, Point(0, 0)), equalTo(emptyList()))

        knightsTours(5, Point(0, 0)).let {
            assertThat(it.size, equalTo(304))
            assertThat(it.filter { tour -> tour.isClosed(5) }.size, equalTo(0))
            assertThat(it, anyElement(equalTo(Tour(
                Point(0, 0), Point(2, 1), Point(4, 0), Point(3, 2), Point(1, 1),
                Point(3, 0), Point(4, 2), Point(3, 4), Point(1, 3), Point(0, 1),
                Point(2, 0), Point(4, 1), Point(2, 2), Point(0, 3), Point(2, 4),
                Point(4, 3), Point(3, 1), Point(1, 0), Point(0, 2), Point(1, 4),
                Point(3, 3), Point(1, 2), Point(0, 4), Point(2, 3), Point(4, 4)
            ))))
            assertThat(it, containsAll(knightsToursLazy(5, Point(0, 0)).toList()))
        }
    }

    @Test fun `knight's tours for larger boards`() {
        val closedTour = knightsToursLazy(6, Point(0, 0)).find { it.isClosed(6) }!!
        assertThat(closedTour.toString(), equalTo(
            "Tour(points=[(0,0), (2,1), (4,0), (3,2), (5,1), (3,0), (1,1), (0,3), (2,2), (0,1), (2,0), (4,1), (5,3), " +
                "(4,5), (2,4), (0,5), (1,3), (2,5), (4,4), (5,2), (3,3), (1,4), (0,2), (1,0), (3,1), (5,0), (4,2), " +
                "(5,4), (3,5), (4,3), (5,5), (3,4), (1,5), (2,3), (0,4), (1,2)])"
        ))
    }
}
Be first to comment
Leave a reply