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; } |
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
Please Login to comment...