7

I had an interview were I was asked a seemingly simple algorithm question: "Write an algorithm to return me all possible winning combinations for tic tac toe." I still can't figure out an efficient way to handle this. Is there a standard algorithm or common that should be applied to similar questions like this that I'm not aware of?

Sebastian
  • 1,834
  • 2
  • 10
  • 22
DroidT
  • 3,168
  • 3
  • 31
  • 29
  • paxdiablo's answer works; you could also approach it from 'the other side': start from a *blank* board, and play out every possible game, keeping track of the final winning positions reached. This would be more work than paxdiablo's answer, but for a more complex game than tic tac toe might turn out to be easier. – AakashM Feb 25 '15 at 11:57
  • Is a winning combination the final board configuration or also the moves up to it? – Sebastian Oct 22 '22 at 07:48

8 Answers8

10

This is one of those problems that's actually simple enough for brute force and, while you could use combinatorics, graph theory, or many other complex tools to solve it, I'd actually be impressed by applicants that recognise the fact there's an easier way (at least for this problem).

There are only 39, or 19,683 possible combinations of placing x, o or <blank> in the grid, and not all of those are valid.

First, a valid game position is one where the difference between x and o counts is no more than one, since they have to alternate moves.

In addition, it's impossible to have a state where both sides have three in a row, so they can be discounted as well. If both have three in a row, then one of them would have won in the previous move.

There's actually another limitation in that it's impossible for one side to have won in two different ways without a common cell (again, they would have won in a previous move), meaning that:

XXX
OOO
XXX

cannot be achieved, while:

XXX
OOX
OOX

can be. But we can actually ignore that since there's no way to win two ways without a common cell without having already violated the "maximum difference of one" rule, since you need six cells for that, with the opponent only having three.

So I would simply use brute force and, for each position where the difference is zero or one between the counts, check the eight winning possibilities for both sides. Assuming only one of them has a win, that's a legal, winning game.


Below is a proof of concept in Python, but first the output of time when run on the process sending output to /dev/null to show how fast it is:

real    0m0.169s
user    0m0.109s
sys     0m0.030s

The code:

def won(c, n):
  if c[0] == n and c[1] == n and c[2] == n: return 1
  if c[3] == n and c[4] == n and c[5] == n: return 1
  if c[6] == n and c[7] == n and c[8] == n: return 1

  if c[0] == n and c[3] == n and c[6] == n: return 1
  if c[1] == n and c[4] == n and c[7] == n: return 1
  if c[2] == n and c[5] == n and c[8] == n: return 1

  if c[0] == n and c[4] == n and c[8] == n: return 1
  if c[2] == n and c[4] == n and c[6] == n: return 1

  return 0

pc = [' ', 'x', 'o']
c = [0] * 9
for c[0] in range (3):
  for c[1] in range (3):
    for c[2] in range (3):
      for c[3] in range (3):
        for c[4] in range (3):
          for c[5] in range (3):
            for c[6] in range (3):
              for c[7] in range (3):
                for c[8] in range (3):
                  countx = sum([1 for x in c if x == 1])
                  county = sum([1 for x in c if x == 2])
                  if abs(countx-county) < 2:
                    if won(c,1) + won(c,2) == 1:
                      print " %s | %s | %s" % (pc[c[0]],pc[c[1]],pc[c[2]])
                      print "---+---+---"
                      print " %s | %s | %s" % (pc[c[3]],pc[c[4]],pc[c[5]])
                      print "---+---+---"
                      print " %s | %s | %s" % (pc[c[6]],pc[c[7]],pc[c[8]])
                      print

As one commenter has pointed out, there is one more restriction. The winner for a given board cannot have less cells than the loser since that means the loser just moved, despite the fact the winner had already won on the last move.

I won't change the code to take that into account but it would be a simple matter of checking who has the most cells (the last person that moved) and ensuring the winning line belonged to them.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 1
    @Pham, possibly, but to what end? We don't need to save time or space here, and using bits means packing and unpacking which is likely to make the code _more_ complex. Unless I've misunderstood, in which case I'd apprciate clarification. – paxdiablo Feb 25 '15 at 07:41
  • I got your point :) totally forgot about space option, so I will take back my comment :) – Pham Trung Feb 25 '15 at 07:45
  • 1
    A difference of one move should not be allowed both ways. The loser can't move after the opponent has won. – Janne Karila Feb 25 '15 at 08:21
  • @JanneKarila, good point, I'll add that into the answer but I won't bother with changing the code. – paxdiablo Feb 25 '15 at 09:40
  • Thanks @paxdiablo for the detailed answer! Would you mind clarifying what is going on with the code, specifically in the last for loop, for us non php developers? – DroidT Feb 25 '15 at 18:39
  • This quite slow... for generating all the possible combinations simply use ``` for i in range(3**9): grid = [i // c for i in range(9)] ``` – UnknownEncoder Apr 13 '23 at 23:49
  • @UnknownEncoder: I generally always optimise for readability first, then worry about performance if it's an *issue.* As you can see from the answer, every winning combination is found in under two-tenths of a second, even with the output. So I suspect readability is the better choice here. – paxdiablo Apr 14 '23 at 00:09
3

Another way could be to start with each of the eight winning positions,

xxx ---
--- xxx
--- --- ... etc.,

and recursively fill in all legal combinations (start with inserting 2 o's, then add an x for each o ; avoid o winning positions):

xxx xxx xxx
oo- oox oox
--- o-- oox ... etc.,
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
1

Today I had an interview with Apple and I had the same question. I couldn't think well at that moment. Later one on, before going to a meeting I wrote the function for the combinations in 15 minutes, and when I came back from the meeting I wrote the validation function again in 15 minutes. I get nervous at interviews, Apple not trusts my resume, they only trust what they see in the interview, I don't blame them, many companies are the same, I just say that something in this hiring process doesn't look quite smart.

Anyways, here is my solution in Swift 4, there are 8 lines of code for the combinations function and 17 lines of code to check a valid board.

Cheers!!!

// Not used yet: 0
// Used with x : 1
// Used with 0 : 2
// 8 lines code to get the next combination
func increment ( _ list: inout [Int], _ base: Int ) -> Bool {
    for digit in 0..<list.count {
        list[digit] += 1
        if list[digit] < base { return true }
        list[digit] = 0
    }
    return false
}
let incrementTicTacToe = { increment(&$0, 3) }

let win0_ = [0,1,2] // [1,1,1,0,0,0,0,0,0]
let win1_ = [3,4,5] // [0,0,0,1,1,1,0,0,0]
let win2_ = [6,7,8] // [0,0,0,0,0,0,1,1,1]
let win_0 = [0,3,6] // [1,0,0,1,0,0,1,0,0]
let win_1 = [1,4,7] // [0,1,0,0,1,0,0,1,0]
let win_2 = [2,5,8] // [0,0,1,0,0,1,0,0,1]
let win00 = [0,4,8] // [1,0,0,0,1,0,0,0,1]
let win11 = [2,4,6] // [0,0,1,0,1,0,1,0,0]
let winList = [ win0_, win1_, win2_, win_0, win_1, win_2, win00, win11]
// 16 lines to check a valid board, wihtout countin lines of comment.
func winCombination (_ tictactoe: [Int]) -> Bool {
    var count = 0
    for win in winList {
        if tictactoe[win[0]] == tictactoe[win[1]],
            tictactoe[win[1]] == tictactoe[win[2]],
            tictactoe[win[2]] != 0 {
            // If the combination exist increment count by 1.
            count += 1
        }
        if count == 2 {
            return false
        }
    }
    var indexes = Array(repeating:0, count:3)
    for num in tictactoe { indexes[num] += 1 }
    // '0' and 'X' must be used the same times or with a diference of one.
    // Must one and only one valid combination
    return abs(indexes[1] - indexes[2]) <= 1 && count == 1
}
// Test
var listToIncrement = Array(repeating:0, count:9)
var combinationsCount = 1
var winCount = 0
while incrementTicTacToe(&listToIncrement) {
    if winCombination(listToIncrement) == true {
        winCount += 1
    }
    combinationsCount += 1
}
print("There is \(combinationsCount) combinations including possible and impossible ones.")
print("There is \(winCount) combinations for wining positions.")
/*
  There are 19683 combinations including possible and impossible ones.
  There are 2032 combinations for winning positions.
*/

listToIncrement = Array(repeating:0, count:9)
var listOfIncremented = ""
for _ in 0..<1000 { // Win combinations for the first 1000 combinations
    _ = incrementTicTacToe(&listToIncrement)
    if winCombination(listToIncrement) == true {
        listOfIncremented += ", \(listToIncrement)"
    }
}
print("List of combinations: \(listOfIncremented)")

/* 
  List of combinations: , [2, 2, 2, 1, 1, 0, 0, 0, 0], [1, 1, 1, 2, 2, 0, 0, 0, 0], 
  [2, 2, 2, 1, 0, 1, 0, 0, 0], [2, 2, 2, 0, 1, 1, 0, 0, 0], [2, 2, 0, 1, 1, 1, 0, 0, 0],
  [2, 0, 2, 1, 1, 1, 0, 0, 0], [0, 2, 2, 1, 1, 1, 0, 0, 0], [1, 1, 1, 2, 0, 2, 0, 0, 0],
  [1, 1, 1, 0, 2, 2, 0, 0, 0], [1, 1, 0, 2, 2, 2, 0, 0, 0], [1, 0, 1, 2, 2, 2, 0, 0, 0],
  [0, 1, 1, 2, 2, 2, 0, 0, 0], [1, 2, 2, 1, 0, 0, 1, 0, 0], [2, 2, 2, 1, 0, 0, 1, 0, 0],
  [2, 2, 1, 0, 1, 0, 1, 0, 0], [2, 2, 2, 0, 1, 0, 1, 0, 0], [2, 2, 2, 1, 1, 0, 1, 0, 0],
  [2, 0, 1, 2, 1, 0, 1, 0, 0], [0, 2, 1, 2, 1, 0, 1, 0, 0], [2, 2, 1, 2, 1, 0, 1, 0, 0],
  [1, 2, 0, 1, 2, 0, 1, 0, 0], [1, 0, 2, 1, 2, 0, 1, 0, 0], [1, 2, 2, 1, 2, 0, 1, 0, 0],
  [2, 2, 2, 0, 0, 1, 1, 0, 0]
*/
ideastouch
  • 1,307
  • 11
  • 6
0

This is a java equivalent code sample

package testit;

public class TicTacToe {

public static void main(String[] args) {
    // TODO Auto-generated method stub
    // 0 1 2
    // 3 4 5
    // 6 7 8
    char[] pc = {' ' ,'o', 'x' };

    char[] c = new char[9];

    // initialize c

    for (int i = 0; i < 9; i++)
        c[i] = pc[0];

    for (int i = 0; i < 3; i++) {
        c[0] = pc[i];
        for (int j = 0; j < 3; j++) {
            c[1] = pc[j];
            for (int k = 0; k < 3; k++) {
                c[2] = pc[k];
                for (int l = 0; l < 3; l++) {
                    c[3] = pc[l];
                    for (int m = 0; m < 3; m++) {
                        c[4] = pc[m];
                        for (int n = 0; n < 3; n++) {
                            c[5] = pc[n];
                            for (int o = 0; o < 3; o++) {
                                c[6] = pc[o];
                                for (int p = 0; p < 3; p++) {
                                    c[7] = pc[p];
                                    for (int q = 0; q < 3; q++) {
                                        c[8] = pc[q];

                                        int countx = 0;
                                        int county = 0;

                                        for(int r = 0 ; r<9 ; r++){
                                            if(c[r] == 'x'){

                                                countx = countx + 1;
                                            }
                                            else if(c[r] == 'o'){

                                                county = county + 1;

                                            }


                                        }

                                        if(Math.abs(countx - county) < 2){

                                            if(won(c, pc[2])+won(c, pc[1]) == 1 ){
                                                System.out.println(c[0] + " " + c[1] + " " + c[2]);
                                                System.out.println(c[3] + " " + c[4] + " " + c[5]);
                                                System.out.println(c[6] + " " + c[7] + " " + c[8]);

                                                System.out.println("*******************************************");


                                            }


                                        }









                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

public static int won(char[] c, char n) {

    if ((c[0] == n) && (c[1] == n) && (c[2] == n))
        return 1;
    else if ((c[3] == n) && (c[4] == n) && (c[5] == n))
        return 1;
    else if ((c[6] == n) && (c[7] == n) && (c[8] == n))
        return 1;
    else if ((c[0] == n) && (c[3] == n) && (c[6] == n))
        return 1;
    else if ((c[1] == n) && (c[4] == n) && (c[7] == n))
        return 1;
    else if ((c[2] == n) && (c[5] == n) && (c[8] == n))
        return 1;
    else if ((c[0] == n) && (c[4] == n) && (c[8] == n))
        return 1;
    else if ((c[2] == n) && (c[4] == n) && (c[6] == n))
        return 1;
    else
        return 0;

}

} `

0

Below Solution generates all possible combinations using recursion

It has eliminated impossible combinations and returned 888 Combinations

Below is a working code Possible winning combinations of the TIC TAC TOE game

const players = ['X', 'O'];
let gameBoard = Array.from({ length: 9 });

const winningCombination = [
  [ 0, 1, 2 ],
  [ 3, 4, 5 ],
  [ 6, 7, 8 ],
  [ 0, 3, 6 ],
  [ 1, 4, 7 ],
  [ 2, 5, 8 ],
  [ 0, 4, 8 ],
  [ 2, 4, 6 ],
];

const isWinningCombination = (board)=> {
  if((Math.abs(board.filter(a => a === players[0]).length - 
  board.filter(a => a === players[1]).length)) > 1) {
    return false
  }
  let winningComb = 0;
  players.forEach( player => {
    winningCombination.forEach( combinations => {
      if (combinations.every(combination => board[combination] === player )) {
        winningComb++;
      }
    });
  });
  return winningComb === 1;
}

const getCombinations = (board) => {
  let currentBoard = [...board];
  const firstEmptySquare = board.indexOf(undefined)
  if (firstEmptySquare === -1) {
    return isWinningCombination(board) ? [board] : [];
  } else {
    return [...players, ''].reduce((prev, next) => {
      currentBoard[firstEmptySquare] = next;
      if(next !== '' && board.filter(a => a === next).length > (gameBoard.length / players.length)) {
        return [...prev]
      }
      return [board, ...prev, ...getCombinations(currentBoard)]
    }, [])

  }
}

const startApp = () => {
  let combination = getCombinations(gameBoard).filter(board => 
      board.every(item => !(item === undefined)) && isWinningCombination(board)
    )
  printCombination(combination)
}

const printCombination = (combination)=> {
  const ulElement = document.querySelector('.combinations');
  combination.forEach(comb => {
    let node = document.createElement("li");
    let nodePre = document.createElement("pre");
    let textnode = document.createTextNode(JSON.stringify(comb));
    nodePre.appendChild(textnode);
    node.appendChild(nodePre); 
    ulElement.appendChild(node);
  })
}
startApp();
Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
0

Could be solved with brute force but keep in mind the corner cases like player2 can't move when player1 has won and vice versa. Also remember Difference between moves of player1 and player can't be greater than 1 and less than 0.

I have written code for validating whether provided combination is valid or not, might soon post on github.

  • 1
    Thank you for your contribute but all you wrote is already covered in @paxdiablo 's answer. – Masoud Keshavarz May 17 '21 at 08:23
  • If player1 is winner then they can't have even equal cells and it's one of the corner case which is not mentioned. Also don't think this needed the down vote else would be difficult for new comers to increase the contribution. – Hariom Singh May 18 '21 at 11:15
0

This discovers all possible combinations for tic tac toe (255,168) -- written in JavaScript using recursion. It is not optimized, but gets you what you need.

const [EMPTY, O, X] = [0, 4, 1]
let count = 0 

let coordinate = [
    EMPTY, EMPTY, EMPTY, 
    EMPTY, EMPTY, EMPTY, 
    EMPTY, EMPTY, EMPTY
]

function reducer(arr, sumOne, sumTwo = null) {
    let func = arr.reduce((sum, a) => sum + a, 0)
    if((func === sumOne) || (func === sumTwo)) return true
}

function checkResult() {
    let [a1, a2, a3, b1, b2, b3, c1, c2, c3] = coordinate
    if(reducer([a1,a2,a3], 3, 12)) return true
    if(reducer([a1,b2,c3], 3, 12)) return true
    if(reducer([b1,b2,b3], 3, 12)) return true
    if(reducer([c1,c2,c3], 3, 12)) return true
    if(reducer([a3,b2,c1], 3, 12)) return true
    if(reducer([a1,b1,c1], 3, 12)) return true
    if(reducer([a2,b2,c2], 3, 12)) return true
    if(reducer([a3,b3,c3], 3, 12)) return true
    if(reducer([a1,a2,a3,b1,b2,b3,c1,c2,c3], 21)) return true
    return false
}

function nextPiece() {
    let [countX, countO] = [0, 0]
    for(let i = 0; i < coordinate.length; i++) {
        if(coordinate[i] === X) countX++
        if(coordinate[i] === O) countO++
    }
    return countX === countO ? X : O
}

function countGames() {
    if (checkResult()) {
        count++
    }else {
        for (let i = 0; i < 9; i++) {
            if (coordinate[i] === EMPTY) {
                coordinate[i] = nextPiece()
                countGames()
                coordinate[i] = EMPTY
            }
        }
    }
}

countGames()
console.log(count)

I separated out the checkResult returns in case you want to output various win conditions.

James Risner
  • 5,451
  • 11
  • 25
  • 47
Tim
  • 21
  • 2
0

Most of the answers for this question are quite slow so here's a faster approach


def won(c, n):
  if c[0] == n and c[1] == n and c[2] == n: return 1
  if c[3] == n and c[4] == n and c[5] == n: return 1
  if c[6] == n and c[7] == n and c[8] == n: return 1

  if c[0] == n and c[3] == n and c[6] == n: return 1
  if c[1] == n and c[4] == n and c[7] == n: return 1
  if c[2] == n and c[5] == n and c[8] == n: return 1

  if c[0] == n and c[4] == n and c[8] == n: return 1
  if c[2] == n and c[4] == n and c[6] == n: return 1

  return 0

for count in range(3**9):
    grid = [(count // (3 ** i)) % 3 for i in range(9)]
    if won(grid, 1) + won(grid, 2) == 1:
       print(grid)