Programming Interview Question: Eight Queens
jetheis.com
jetheis.com
It can actually be done fairly quickly if you really only ask for 8x8 instead of a general NxN solver, and the person realizes one key thing: computers have gotten ridiculously fast, so that if you just bang out the first dumb thing that comes to mind, you can probably be done and have the answers before the people with the clever, general, efficient solutions get their solutions coded or even designed.
For instance, this brute force, ridiculously ugly, un-clever, only-works-for-8x8 solution took about 5 minutes or so to write, with half that spent deciding how to number the diagonals and looking at a couple of squares on a chessboard to see how rank/file maps to my diagonal numbering:
#include <stdio.h>
int main(void)
{
unsigned long pos;
for (pos = 0; pos <= 077777777; ++pos)
{
unsigned long tmppos = pos;
unsigned long taken = 0;
int rank;
int file;
for (file = 0; file < 8; ++file)
{
unsigned long mask;
rank = tmppos & 07;
mask = (1L << file) | /* file */
(1L << (rank + 8)) | /* rank */
(1L << (23 + file - rank)) /* diagonal */
| (1L << (45 - file - rank)); /* other diagonal */
if (taken & mask)
goto fail;
taken |= mask;
tmppos >>= 3;
}
printf("%lo\n", pos);
fail: ;
}
return 0;
}
It takes under 0.5 seconds on my 2009 Mac Pro when compiled without optimization, and about half that with optimization.(To be completely honest, I rarely write C nowadays, so although it only took a few minutes to write, the damn thing took half an hour to debug. I wrote "1 <<" instead of "1L <<" because I thought for some reason that int was 64 bit nowadays).
The funniest thing about puzzle interviews as an observer is I can't stop thinking ... "and if he gets the job he's going to spend 12 hour days writing a boring frameworked CRUD app" ... "this poor noob probably thinks real on the job programming is like solving chessboard puzzles all day long LOL".
The unfortunate part about the abstract puzzle fad is almost no one is paid to solve abstract puzzles but no one is tested on real puzzles. "Yes I know quite well traveling salesman is infeasible for 48 states, I'm asking you to solve it for 5 sites because we only have 5 warehouses" etc.
http://en.wikipedia.org/wiki/Travelling_salesman_problem#His... "In 2006, Cook and others computed an optimal tour through an 85,900-city instance given by a microchip layout problem, currently the largest solved TSPLIB instance. For many other instances with millions of cities, solutions can be found that are guaranteed to be within 2-3% of an optimal tour."
Once you're discussing real things, they aren't puzzles anymore. One of my favorite things to do in interviews is to just actually have a discussion, as peers, about a real-world technical problem with multiple potential tradeoffs and no one right answer.
If they can't get the naive brute force solution in a few minutes, that indicates something pretty bad. If they treat it as a permutation question and get the n! brute force answer, that is much better (and is still something that can be done in about the same amount of time. If you treat the problem as numbered permutations then checking for diagonals even becomes non-tedious, making it easier to write out on a whiteboard than the naive brute force solution).
If they get a proper solution in only a few minutes, you know that they either have their act together, or that they have seen the problem before and remember the solution. That in itself is a good indicator, though if they get a proper solution right away there should be some follow-up questions to get a handle on how exactly they were able to solve it that quickly.
Real world problems just aren't practical in interviews, you'll never get to the point where their programming chops are really being tested.
The purpose of this question isn't to tell you whether the candidate can program chess boards or not. It could be used to help find candidates who:
* Have experience with search algorithms
* Specifically have experience with backtracking solutions
* Can reason through a complex problem (and communicate while doing so)
* Paid attention in school / knowledge retention (for recent grads)
This was a real problem for me as a programmer early in my career and a source of great anxiety on interviews. I've gotten better at it later in life almost solely because I needed to do so for interviewing purposes, but to be honest, I don't think it is a real world problem if someone isn't great at simultaneously reasoning through a complex problem and communicating while doing so. I suspect there is a large group of people like me who are very experienced and knowledgeable programmers who may come off poorly in whiteboard-coding interviews because actively focusing on complex problems makes it difficult for them to interact in the real world.
I do think being able to communicate is key for developers, but even when I had issues with this I could very clearly communicate how I went about solving the complex problem after I had finished it (or at least was convinced I was on the right track with one or two solutions and my mind wasn't running through the problem on dozens of different threads anymore) and it never impacted any real world collaboration because it is extremely rare that you ever sit around with a bunch of other programmers trying to reason through exactly the same algorithm at the same time, and even when that does happen, IME it is better to just get a few independent solutions and then communicate about the pros and cons of each rather than just "brainstorm" in real-time.
However, I'd be pissed off if the job turns out to be like 99% of most jobs, writing boring CRUD enterprise apps.
The trick to solving things fast is to use the structure of the problem to make your problem solving procedure less general. That is, you do premature optimization, except instead of trading readability for runtime speed, your are trading readability for program length.
With real problems, there is a similar tradeoff, so I think this is a useful skill to demonstrate. But it does take practice to find the tradeoff that works best for interview questions, which is not the same as for real life coding.
In my solution, I use mutable state to reduce the amount of code. board is mutated to avoid the boilerplate of copying objects. valid is mutated as a kind of optimization, but the real reason is it makes the program simpler.
def complete_board(board, l):
"""If possible, completes a board (whose first l rows
have been filled with 1 queen per row). Returns
true if this is possible, false otherwise
board is a list of ints representing the position
(0-based) of each queen"""
N = len(board)
if l == N:
return True
# determine the valid positions for a queen in the r'th row
valid = [True] * N
for row, col in enumerate(board[:l]):
valid[col] = False
s = l - row
if col + s < N:
valid[col + s] = False
if col - s >= 0:
valid[col - s] = False
for col, v in enumerate(valid):
if v:
board[l] = col
if complete_board(board, l + 1):
return True
return FalseI do have to say that as someone who sometimes conducts technical interviews, I'm not sure I would actually give this question to an interviewee, unless I was leaving him or her alone to work for a couple of hours, because of how long it would probably take to reach an answer.
So it appears they are aware of that problem. I'm of the opinion that some "non-classical" problem is probably a better indication of skill and reasoning. Work through a known problem you had at work (which the candidate is hopefully not too familiar with, otherwise abort and try another), and see how they reason out the problem, then ask for some code that might fix it.
The catch was that I had to only count the solutions, not find them. The very inexperienced engineer that I was, wrote a highly-optimized (bit boards) solution that could solve N=12 in 6 seconds. But I got this solution after the fact, and it took me hours to write it. A more qualified software engineer would have found a proper combinatorial solution in minutes.
In general, giving an actual NP-Complete problem in an interview question and expecting a good solution is unrealistic. However hiding an easy problem in a hard-looking one seems pretty effective.
This particular problem lends itself really well to functional or constraint logic solutions, though (e.g. http://www.cs.nott.ac.uk/~rxq/files/8nQueenModel.pdf) -- I'd give bonus points to a candidate who could identify that this problem is well suited to one of those toolsets instead of immediately whipping out ruby, python, or js.
-when searching is it better to use copy-make or move/undo approach ? (copy make is faster on modern pc's, at least for things like 8Q's, it's also much easier to implement as you don't need any code for undoing moves)
-what is the best way to check if given move (placing the queen) is valid ? (you need three 0-1 arrays for rows and diagonals in both direction this a neat trick, very difficult to come up with but which speeds the whole thing immensely)
-how do you break out of the recursion if the task is changed to find one solution ?
I know all those things because I am a hobbyist who wrote a lot of solvers and dabbled into chess programming. I am probably in top 0.1% of people if I get question like this. I am incompetent when it comes to things needed in most commercial applications as I've never worked on big commercial projects. This question filters for people like me. I can't imagine how this a good thing for anything practical (unless the task is to produce the best chess program or something).
1)having only one board and make/undo moves on it. So every time you put a new queen on the board you add it to the representation of the board and every time you go back in recursion you need to do an undo before exiting the function (so you new branch won't have queens placed on the board from previous branch)
2)storing as many boards as many steps deep in the recursion you are (max 8 in 8 queens problem) so every time you go deeper into recursion you create new board then you copy current state there and then you make a move (place a queen) on this new board; this way when you go back in recursion you do nothing as this new board is taken care of by stack pointer going back.
The cost of copy make is memory (you need memory for new board representation for every step deeper into recursion) but it's usually not much of a problem. The benefit is that you don't need to code undo logic (which in case of some games is quite complicated) and undo is much faster which usually outweights costs of copying the memory. Some top chess programs are move/undo and some are copy/make, chess board representation usually takes about 200 bytes and variations may go as deep as 100 half moves which means you may need like 20kb additional memory for every thread doing searching. In case of 8 queens it's of course trivial though.
Some more information (for chess, so way more advanced):
https://chessprogramming.wikispaces.com/Copy-Make
This is a great point that I realized that when I sat down to write up a solution myself. The video-explanation-version of coding it up for the CFI course ended up being 60 minutes long.
Suspect when it does get asked in an interview context they might only want to see some of your problem solving process and algorithm devising, not a full code-up.
When I was 16 and learning how to program in Java, there was an online competition for high schoolers, and this was one of the introductory programming questions. Your program had to execute in under a time limit (5 seconds, N=13), and I had come up with the best solution I could and was still at 7 seconds. It took me the rest of the month spending hours a day reading on speed optimizations in order to cut down every unnecessary instruction, and remove every intermediate variable. Eventually, the program executed in 4.9 seconds.
While quite interesting, the problem only took an afternoon to implement the algorithm. What differentiated my chops from my competitors was that I could improve my program beyond the first implementation.
But that's a very good exercice.
> I am first year maths student at Uni Warsaw
then Math is your major discipline. Please write down how you would express the solution in a pure mathematical form.
I guess expressing it mathematically purely is already done in the code of the original solution, because in a way program == proof. If you have some time, here's a great read on this: http://www.maa.org/sites/default/files/pdf/upload_library/22...
Bear in mind that the objective isn't to weed out people who can't solve it. There are plenty of people who are fine coders who wouldn't quickly hit the solution for any number of reasons, including nervousness. The objective is to weed out the charlatans and amateurs who managed to get past the HR resume filter, so that as little time as possible is wasted.
I got it on my second try, though maybe I just got lucky.
This was a computer engineering course, and I was a computer science student with no background in Verilog or VHDL. I think it took me about 30 hours to get a working solution. However, my solution is very limited, and shows how unfamiliar I was with thinking in hardware terms; other students in the course produced solutions which did not have my limitations, and they did it with much less effort. When I mentioned that to another student in the course, he looked surprised. When I mentioned I was a CS student with no prior experience with FPGAs, Verilog or VHDL, he said, oh, that's pretty good!
http://therandombit.blogspot.com/2008/09/n-queens-implmented...
http://en.wikipedia.org/wiki/Eight_queens_puzzle#Explicit_so...
As prep it's a concise problem you can do on your own time to get your brain working over basic things that you may not have been thinking about lately.
As a question to actually give someone (or receive) during an interview? Seems way to long.
Edit: typo
The tricky part is permuting the array. I used a recursive swap routine, swapping to the right and testing if the array were valid to the left of i then recursing i+1. If it fails you don't have to recurse, you can pop, trimming the search tree drastically. There were 8 or 9 solutions if I remember right, less if you eliminate rotations and mirrors. {edit: 12 solutions}
I think it's an interesting puzzle, but I don't think that makes it a good interview question.
The preliminaries are that we represent a board position containing N queens on an NxN board by a list of row positions in each of N columns.
newtype Board = Board [Int] deriving ( Eq, Show )
emptySolution = Board []
Then, given any particular NxN board with M queens in first M columns that is a solution to M-queens, we can generate the list of possible new boards with (M+1) queens. We try all possible row positions for the new column excepting the rows where queens already exist. attempts :: Int -> Board -> [Board]
attempts n (Board queens) =
map (\r -> Board (r : queens)) ([0 .. n - 1] \\ queens)
And we prune out the ones where the new queen has a diagonal conflict. valid :: Board -> Bool
valid (Board []) = True
valid (Board (q:cols)) =
and $ map (\(n, c) -> abs (q - c) /= n) (zip [1..] cols)
-- this is a bit clever, as we walk back through
-- the previous columns, if the absolute difference
-- between the row position of the new queen and
-- an old queen is ever the count of the column
-- we're in, then it must be in a diagonal line
-- from the new queen
All of the above is pretty standard, but the fun part is using the list monad to structure a search. Each bind (<-) in do-notation can be thought of as non-deterministically setting a particular choice from the right side to the name on the left queens :: Int -> [Board]
queens n = search n [emptySolution] where
search 0 boards = boards -- no more queens to add
search k boards = do
board <- search (k - 1) boards -- find a smaller solution
filter valid (attempts board) -- try all possible extensions,
-- remove the bad ones
>>> length (queens 8)
92
Finally, the pattern of `search` is really common, it's just iterating the generation of attempted solutions. We can write it with the built-in function `Control.Monad.foldM` or another common utility combinator, `iterateM` iterateM :: Monad m => (a -> m a) -> a -> [m a]
iterateM f x = iterate (f =<<) (return x)
queens n = iterateM (filter valid . attempts n) emptySolution !! n
Finally, it's worth noting that there's nothing special about the list monad's implementation. It's easy to rewrite it without using the typeclass. -- this is the list monad definition
instance Monad [] where
return a = [a]
xs >>= f = concat (map f xs)
iterateList :: (a -> [a]) -> a -> [[a]]
iterateList f x = iterate (concat . map f) [x]I'll go column by column, noting symmetry that also means I only need to check half the rows for the first queen, and with symmetry and the squares she eliminates I'll have fewer options to consider on each subsequent column.
+---+---+---+---+
| Q | X | X | X |
+---+---+---+---+
| | X | | |
+---+---+---+---+
| | | X | |
+---+---+---+---+
| | | | X |
+---+---+---+---+
First queen placed, add the second we only have two options so
I'll try the top one. +---+---+---+---+
| Q | X | X | X |
+---+---+---+---+
| | X | X | |
+---+---+---+---+
| | Q | X | X |
+---+---+---+---+
| | | X | X |
+---+---+---+---+
Oops, gotta move the second queen: +---+---+---+---+
| Q | X | X | X |
+---+---+---+---+
| | X | Q | X |
+---+---+---+---+
| | | X | X |
+---+---+---+---+
| | Q | X | X |
+---+---+---+---+
Uh oh, can't place the 4th. Moving the first queen since we've
exhausted all placements of the 3rd and 2nd queen with the 1st in
the upper corner. +---+---+---+---+
| | X | | |
+---+---+---+---+
| Q | X | X | X |
+---+---+---+---+
| | X | | |
+---+---+---+---+
| | | X | |
+---+---+---+---+
+---+---+---+---+
| | X | Q | X |
+---+---+---+---+
| Q | X | X | X |
+---+---+---+---+
| | X | X | Q |
+---+---+---+---+
| | Q | X | X |
+---+---+---+---+
I placed the remainder at once because there was only one legal
placement in each column.Extending this approach to 8x8 isn't difficult if the challenge is merely to find a solution.
EDIT: I found it in 3 attempts on the 8x8. Going to the column with the most constraints (fewest options) to minimize my branching.
Each row must have a queen, and each row must have the queen in a different position. Therefore there are 8 rows and we just need to find a correct permutation of them.
Number each row 0 through 7. Generate permutations of these numbers.
To check for diagonal collisions between permutation indices x and y: abs(perm[x]-perm[y]) == abs(x-y)
(In other words, if rows 2 and 4 cannot be two places apart, rows 6 and 7 cannot be one place apart.)
Check for diagonal collisions as you're writing out possible valid permutations by hand. Doesn't take long to generate solutions since you don't have to actually draw or mentally picture a board to work out diagonal collisions. You just need to keep 8 single digit numbers in your head.
import Control.Monad
queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n] safe x [] n = True safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]
main = mapM_ print $ queens 8
---- http://en.wikibooks.org/wiki/Algorithm_Implementation/Miscel...
import Control.Monad
queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n]
safe x [] n = True
safe x (c:y) n = and [ x /= c, x /= c + n, x /= c - n, safe x y (n+1)]
main = mapM_ print $ queens 8But you're right, examples like this are extremely poor marketing for Haskell. It's really not how you'd write most Haskell programs. A lot of the Haskell code on sites like Rosetta Code is deliberately terse to show off.
I changed the indentation a little bit, to make it more clear:
queens n = foldM
(\y _ -> [x:y | x <- [1..n], safe x y 1])
[]
[1..n]
safe x [] n = True
safe x (c:y) n = and [ x /= c
, x /= c + n
, x /= c - n
, safe x y (n+1)
]
main = mapM_ print $ queens 8
'queens' is essentially a for loop that concatenates the elements between 1..n that are safe, and 'safe' is a function that returns true if it's given an empty list, or if the input satisfies all of the predicates in the other list (notice the recursion that's possible because of laziness.) The function 'and' that it calls takes a list of bools and returns true if all of them are true. 'main' prints each list that results from running 'queens 8'There is some syntax and convention you have to learn, e.g. what the _ in mapM_ means, what a list comprehension is, or what : means, etc. but once you have it's a fairly elegant solution (compare to the code snippets from imperative languages.)
What the actual fuck ((if b[r]?[c] then c else -1) for c in [0..n-1]).filter (c) -> c >= 0
I am aware of what the line is doing, I just have no idea what b & c are. Best guess; Board & Column.