3
\$\begingroup\$

The N-Queens puzzle can be solved in many ways. One is by a depth-first-search backtracking algorithm.

Below is my generalized version using mutual recursion:

let dfsBacktrack initialValue solved getNextItems = 

    let rec procCurrent path =
        match solved path with
        | true -> Some path
        | false -> procNext path (getNextItems path) 

    and procNext path nextItems =
        match nextItems with
        | [] -> None
        | next::tail ->
            match procCurrent (next::path) with
            | Some solution -> Some solution
            | None -> procNext path tail

    procCurrent [ initialValue ]

The main problem with it is that it is not tail recursive so it builds up the stack with the limitations that leads to.

Anyway solving the N-Queens problem with it could look like:

let queensProblem n = 
    if n < 4 then failwith "n must be greater than or equal to 4"

    let solved queens = queens |> List.length = n

    let getFields row = [0..n-1] |> List.map (fun col -> col, row)

    let isValidField (col, row) (qcol, qrow) = 
        col <> qcol && row <> qrow // horz and vert
        && abs (col - qcol) <> abs (row - qrow) // diagonals

    let reduceByQueen fields queen =
        fields
        |> List.where (fun pos -> isValidField pos queen)

    let getValidFields queens = 
        let row = queens |> List.head |> (snd)
        queens
        |> List.fold reduceByQueen (getFields (row + 1))

    dfsBacktrack (0, 0) solved getValidFields |> Option.get |> List.rev

For n < 27 it finds a solution fairly quickly.

For n = 8 it produces the output: [(0, 0); (4, 1); (7, 2); (5, 3); (2, 4); (6, 5); (1, 6); (3, 7)]

The above algorithm can also be used to find a solution for the Knight's Tour-problem:

let knightsTour n = 
    if n < 5 then failwith "n must be greater than or equal to 5"

    let solved positions = positions |> List.length = n*n
    let onboard (x, y) = x >= 0 && x < n && y >= 0 && y < n
    let offsets = [ (2, 1); (2, -1); (1, 2); (1, -2); (-1, 2); (-1, -2); (-2, 1); (-2, -1) ]
    let offset (x, y) (dx, dy) = x + dx, y + dy

    let nextPositions path = 
        let pos = path |> List.head
        let rec collect pos path recall = 
            let posses = 
                offsets 
                |> List.map (fun o -> offset pos o)
                |> List.where (fun p -> onboard p && not (path |> List.contains p))

            // Warnsdorff's rule
            match recall with
            | true -> 
                posses
                |> List.map (fun pt -> pt, collect pt (pt::path) false |> List.length)
                |> List.sortBy (fun (_, count) -> count)
                |> List.map (fun (pt, _) -> pt)
            | false -> posses

        collect pos path true

    dfsBacktrack (0, 0) solved nextPositions 

Another algorithm for the N-Queens problem is the Min-conflicts algorithm, which shows to be much more efficient:

let queensProblemByMinConflicts maxIterations n =
    if n < 4 then failwith "n must be greater than or equal to 4"

    let rand = Random(5)

    let initQueens = 
        Array.init n id 
        |> Array.map (fun i -> i, (i * 2) % n) // map (fun i -> i, rand.Next() % n)  //

    let hasConflict (q1c, q1r) (q2c, q2r) = q1c = q2c || q1r = q2r || abs (q1c - q2c) = abs (q1r - q2r)

    let getConflicts queen queens = 
        let conflicts = 
            queens 
            |> Array.where (fun q -> q <> queen && hasConflict queen q)

        match conflicts with
        | [||] -> None
        | arr -> Some (arr)

    let countConflicts queen queens = 
        match getConflicts queen queens with
        | None -> 0
        | Some conflicts -> conflicts |> Array.length

    let indicesWithConflicts queens = 
        queens 
        |> Array.mapi (fun i q -> (i, q))
        |> Array.where (fun (_, q) -> countConflicts q queens > 0)
        |> Array.map (fst)

    let minimizeConflicts (qc, qr) queens = 
        [| 0..n-1 |]
        |> Array.where (fun r -> r <> qr)
        |> Array.map (fun r -> (qc, r))
        |> Array.groupBy (fun q -> countConflicts q queens)
        |> Array.sortBy (fun (k, _) -> k)
        |> Array.head 
        |> fun (_, qs) -> qs.[rand.Next(0, qs |> Array.length)]

    let rec nextState queens iteration =
        match indicesWithConflicts queens with
        | [||] -> Some queens
        | conflicts -> 
            match iteration with
            | _ when iteration = maxIterations -> None
            | _ -> 
                let curQueenIndex = conflicts.[rand.Next(0, conflicts |> Array.length)]
                let curQueen = minimizeConflicts queens.[curQueenIndex] queens
                queens.[curQueenIndex] <- curQueen
                nextState queens (iteration + 1)

    nextState initQueens 0

From an initial full - most likely invalid - configuration of N queens on the board (one queen per column), it recursively chooses randomly a queen with conflicts and positions that in its column on a field with the smallest number of conflicts. If more fields have the same smallest number of conflicts one is randomly chosen among them. In the above algorithm maxIterations makes it possible to return with no valid solution found instead of just continue infinitely. nextState is tail recursive.

With let rand = Random(5) and n = 8 the following result is produced:

[|(0, 4); (1, 1); (2, 3); (3, 5); (4, 7); (5, 2); (6, 0); (7, 6)|]

I'm interested in any comment, but improvements of and tips about the use of F# as a language are very welcome.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy