Designing algorithm to solve Ball Sort Puzzle
In Ball Sort Puzzle game, we have p balls of each colour and n different colours, for a total of p×n balls, arranged in n stacks. In addition, we have 2 empty stacks. A maximum of p balls can be in any stack at a given time. The goal of the game is to sort the balls by colour in each of the n stacks.
Rules:
- Only the top ball of each stack can be moved.
- A ball can be moved on top of another ball of the same colour
- A ball can be moved in an empty stack.
Refer to the following GIF for an example game play (Level-7):

Level 7 Gameplay
Approach I [Recursion and BackTrack]:
- From the given rules, a simple recursive algorithm could be generated as below:
- Start with the given initial position of all the balls
- Create an initial empty Queue.
- loop:
- If the current position is sorted:
- return
- else
- Enqueue all possible moves in a Queue.
- Dequeue the next move from the Queue.
- Go to loop.
- If the current position is sorted:
However, the approach looks simple and correct, it has few caveats:
- Incorrect:
- We might end up in an infinite loop if there are >1 moves in the Queue which lead to the same position of balls.
- Inefficient:
- We might end up visiting the same position multiple times.
Thus, eliminating the above-mentioned bottlenecks would solve the issue.
Approach II [Memoization using HashMap]:
- Assumptions:
- We’ll represent ball positions as a vector of strings: {“gbbb”, “ybry”, “yggy”, “rrrg”}
- Create a set called Visited of <String> which will contain the visited positions as one long string.
- Create an empty vector for Answer which will store positions<a, b> of the tubes to move the top ball from tube a to and put it in tube b.
- Initialise grid with the initial settings of the balls.
- func solver(grid):
- add grid to Visited
- loop over all the stacks (i):
- loop over all the stacks (j):
- If move i->j is valid, create newGrid with that move.
- if the balls are sorted in newGrid,
- update Answer;
- return;
- if newGrid is NOT in Visited
- solver(newGrid)
- if solved:
- update Answer
- if the balls are sorted in newGrid,
- If move i->j is valid, create newGrid with that move.
- loop over all the stacks (j):
Sample Game Input I:

Level 3
Sample Input I:
5 ybrb byrr rbyy
Sample Output I:
Move 1 to 4 1 times Move 1 to 5 1 times Move 1 to 4 1 times Move 2 to 5 2 times Move 1 to 2 1 times Move 3 to 1 1 times Move 1 to 2 1 times Move 3 to 1 1 times Move 2 to 1 3 times Move 2 to 3 1 times Move 3 to 4 1 times Move 3 to 2 1 times Move 2 to 4 1 times Move 3 to 5 1 times
Sample Game Input II:

Level 5
Sample Input II:
6 gbbb ybry yggy rrrg
Sample Output II:
Move 1 to 5 3 times Move 2 to 6 1 times Move 3 to 6 1 times Move 1 to 3 1 times Move 2 to 1 1 times Move 2 to 5 1 times Move 2 to 6 1 times Move 3 to 2 3 times Move 3 to 6 1 times Move 4 to 2 1 times Move 1 to 4 1 times
Refer to the below C++ implementation with the comments for the reference:
C++
// C++ program for the above approach #include <bits/stdc++.h> using namespace std; using Grid = vector<string>; Grid configureGrid(string stacks[], int numberOfStacks) { Grid grid; for ( int i = 0; i < numberOfStacks; i++) grid.push_back(stacks[i]); return grid; } // Function to find the max int getStackHeight(Grid grid) { int max = 0; for ( auto stack : grid) if (max < stack.size()) max = stack.size(); return max; } // Convert vector of strings to // canonicalRepresentation of strings string canonicalStringConversion(Grid grid) { string finalString; sort(grid.begin(), grid.end()); for ( auto stack : grid) { finalString += (stack + ";" ); } return finalString; } // Function to check if it is solved // or not bool isSolved(Grid grid, int stackHeight) { for ( auto stack : grid) { if (!stack.size()) continue ; else if (stack.size() < stackHeight) return false ; else if (std::count(stack.begin(), stack.end(), stack[0]) != stackHeight) return false ; } return true ; } // Check if the move is valid bool isValidMove(string sourceStack, string destinationStack, int height) { // Can't move from an empty stack // or to a FULL STACK if (sourceStack.size() == 0 || destinationStack.size() == height) return false ; int colorFreqs = std::count(sourceStack.begin(), sourceStack.end(), sourceStack[0]); // If the source stack is same colored, // don't touch it if (colorFreqs == height) return false ; if (destinationStack.size() == 0) { // If source stack has only // same colored balls, // don't touch it if (colorFreqs == sourceStack.size()) return false ; return true ; } return ( sourceStack[sourceStack.size() - 1] == destinationStack[destinationStack.size() - 1]); } // Function to solve the puzzle bool solvePuzzle(Grid grid, int stackHeight, unordered_set<string>& visited, vector<vector< int > >& answerMod) { if (stackHeight == -1) { stackHeight = getStackHeight(grid); } visited.insert( canonicalStringConversion(grid)); for ( int i = 0; i < grid.size(); i++) { // Iterate over all the stacks string sourceStack = grid[i]; for ( int j = 0; j < grid.size(); j++) { if (i == j) continue ; string destinationStack = grid[j]; if (isValidMove(sourceStack, destinationStack, stackHeight)) { // Creating a new Grid // with the valid move Grid newGrid(grid); // Adding the ball newGrid[j].push_back(newGrid[i].back()); // Adding the ball newGrid[i].pop_back(); if (isSolved(newGrid, stackHeight)) { answerMod.push_back( vector< int >{ i, j, 1 }); return true ; } if (visited.find( canonicalStringConversion(newGrid)) == visited.end()) { bool solveForTheRest = solvePuzzle(newGrid, stackHeight, visited, answerMod); if (solveForTheRest) { vector< int > lastMove = answerMod[answerMod.size() - 1]; // Optimisation - Concatenating // consecutive moves of the same // ball if (lastMove[0] == i && lastMove[1] == j) answerMod[answerMod.size() - 1] [2]++; else answerMod.push_back( vector< int >{ i, j, 1 }); return true ; } } } } } return false ; } // Checks whether the grid is valid or not bool checkGrid(Grid grid) { int numberOfStacks = grid.size(); int stackHeight = getStackHeight(grid); int numBallsExpected = ((numberOfStacks - 2) * stackHeight); // Cause 2 empty stacks int numBalls = 0; for ( auto i : grid) numBalls += i.size(); if (numBalls != numBallsExpected) { cout << "Grid has incorrect # of balls" << endl; return false ; } map< char , int > ballColorFrequency; for ( auto stack : grid) for ( auto ball : stack) if (ballColorFrequency.find(ball) != ballColorFrequency.end()) ballColorFrequency[ball] += 1; else ballColorFrequency[ball] = 1; for ( auto ballColor : ballColorFrequency) { if (ballColor.second != getStackHeight(grid)) { cout << "Color " << ballColor.first << " is not " << getStackHeight(grid) << endl; return false ; } } return true ; } // Driver Code int main( void ) { // Including 2 empty stacks int numberOfStacks = 6; std::string stacks[] = { "gbbb" , "ybry" , "yggy" , "rrrg" , "" , "" }; Grid grid = configureGrid( stacks, numberOfStacks); if (!checkGrid(grid)) { cout << "Invalid Grid" << endl; return 1; } if (isSolved(grid, getStackHeight(grid))) { cout << "Problem is already solved" << endl; return 0; } unordered_set<string> visited; vector<vector< int > > answerMod; // Solve the puzzle instance solvePuzzle(grid, getStackHeight(grid), visited, answerMod); // Since the values of Answers are appended // When the problem was completely // solved and backwards from there reverse(answerMod.begin(), answerMod.end()); for ( auto v : answerMod) { cout << "Move " << v[0] + 1 << " to " << v[1] + 1 << " " << v[2] << " times" << endl; } return 0; } |
Python3
def configureGrid(stacks, numberOfStacks): grid = [] for i in range (numberOfStacks): grid.append(stacks[i]) return grid # Function to find the max def getStackHeight(grid): max = 0 for stack in grid: if max < len (stack): max = len (stack) return max # Convert vector of strings to # canonicalRepresentation of strings def canonicalStringConversion(grid): finalString = "" grid.sort() for stack in grid: finalString + = (stack + ";" ) return finalString # Function to check if it is solved # or not def isSolved(grid, stackHeight): for stack in grid: if len (stack) = = 0 : continue elif len (stack) < stackHeight: return False elif stack.count(stack[ 0 ]) ! = stackHeight: return False return True # Check if the move is valid def isValidMove(sourceStack, destinationStack, height): # Can't move from an empty stack # or to a FULL STACK if len (sourceStack) = = 0 or len (destinationStack) = = height: return False colorFreqs = sourceStack.count(sourceStack[ 0 ]) # If the source stack is same colored, # don't touch it if colorFreqs = = height: return False if len (destinationStack) = = 0 : # If source stack has only # same colored balls, # don't touch it if colorFreqs = = len (sourceStack): return False return True return sourceStack[ len (sourceStack) - 1 ] = = destinationStack[ len (destinationStack) - 1 ] # Function to solve the puzzle def solvePuzzle(grid, stackHeight, visited, answerMod): if stackHeight = = - 1 : stackHeight = getStackHeight(grid) visited.add(canonicalStringConversion(grid)) for i in range ( len (grid)): # Iterate over all the stacks sourceStack = grid[i] for j in range ( len (grid)): if i = = j: continue destinationStack = grid[j] if isValidMove(sourceStack, destinationStack, stackHeight): # Creating a new Grid # with the valid move newGrid = list (grid) # Adding the ball newGrid[j] + = newGrid[i][ len (newGrid[i]) - 1 ] # Removing the ball newGrid[i] = newGrid[i][: - 1 ] if isSolved(newGrid, stackHeight): answerMod.append([i, j, 1 ]) return True if canonicalStringConversion(newGrid) not in visited: if solvePuzzle(newGrid, stackHeight, visited, answerMod): lastMove = answerMod[ len (answerMod) - 1 ] # Optimisation - Concatenating # consecutive moves of the same # ball if lastMove[ 0 ] = = i and lastMove[ 1 ] = = j: answerMod[ len (answerMod) - 1 ][ 2 ] + = 1 else : answerMod.append([i, j, 1 ]) return True return False # Checks whether the grid is valid or not def checkGrid(grid): numberOfStacks = len (grid) stackHeight = getStackHeight(grid) numBallsExpected = ((numberOfStacks - 2 ) * stackHeight) # Cause 2 empty stacks numBalls = 0 for i in grid: numBalls + = len (i) if numBalls ! = numBallsExpected: print ( "Grid has incorrect # of balls" ) return False ballColorFrequency = {} for stack in grid: for ball in stack: if ball in ballColorFrequency: ballColorFrequency[ball] + = 1 else : ballColorFrequency[ball] = 1 for ballColor in ballColorFrequency: if ballColorFrequency[ballColor] ! = getStackHeight(grid): print ( "Color" , ballColor, "is not" , getStackHeight(grid)) return False return True # Driver Code if __name__ = = "__main__" : # Including 2 empty stacks numberOfStacks = 6 stacks = [ "gbbb" , "ybry" , "yggy" , "rrrg" , " ", " "] grid = configureGrid(stacks, numberOfStacks) if not checkGrid(grid): print ( "Invalid Grid" ) exit() if isSolved(grid, getStackHeight(grid)): print ( "Problem is already solved" ) exit() visited = set () answerMod = [] # Solve the puzzle instance solvePuzzle(grid, getStackHeight(grid), visited, answerMod) # Since the values of Answers are appended # When the problem was completely # solved and backwards from there answerMod.reverse() for v in answerMod: print ( "Move" , v[ 0 ] + 1 , "to" , v[ 1 ] + 1 , v[ 2 ], "times" ) |
Move 1 to 5 3 times Move 2 to 6 1 times Move 3 to 6 1 times Move 1 to 3 1 times Move 2 to 1 1 times Move 2 to 5 1 times Move 2 to 6 1 times Move 3 to 2 3 times Move 3 to 6 1 times Move 4 to 2 1 times Move 1 to 4 1 times
Time Complexity: O(n!) where n is the number of stacks.
Auxiliary Space: O(n^2)
Please Login to comment...