Minimum Moves to Spread Stones Over Grid
MedPrompt
You are given a 0-indexed 2D integer matrix grid of size 3 * 3, representing the number of stones in each cell. The grid contains exactly 9 stones, and there can be multiple stones in a single cell.
In one move, you can move a single stone from its current cell to any other cell if the two cells share a side.
Return the minimum number of moves required to place one stone in each cell.
Example 1:
Input: grid = [[1,1,0],[1,1,1],[1,2,1]]
Output: 3
Explanation: One possible sequence of moves to place one stone in each cell is:
1- Move one stone from cell (2,1) to cell (2,2).
2- Move one stone from cell (2,2) to cell (1,2).
3- Move one stone from cell (1,2) to cell (0,2).
In total, it takes 3 moves to place one stone in each cell of the grid.
It can be shown that 3 is the minimum number of moves required to place one stone in each cell.Example 2:
Input: grid = [[1,3,0],[1,0,0],[1,0,3]]
Output: 4
Explanation: One possible sequence of moves to place one stone in each cell is:
1- Move one stone from cell (0,1) to cell (0,2).
2- Move one stone from cell (0,1) to cell (1,1).
3- Move one stone from cell (2,2) to cell (1,2).
4- Move one stone from cell (2,2) to cell (2,1).
In total, it takes 4 moves to place one stone in each cell of the grid.
It can be shown that 4 is the minimum number of moves required to place one stone in each cell.
Constraints:
grid.length == grid[i].length == 30 <= grid[i][j] <= 9- Sum of
gridis equal to9.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach formulates the problem as a classic assignment problem. We need to move extra stones from 'source' cells (more than one stone) to 'destination' cells (zero stones). The goal is to find a one-to-one mapping between the extra stones and empty cells that minimizes the total number of moves. Since the number of stones to move is small (at most 8), we can explore every possible assignment. This is equivalent to generating all permutations of the destination cells and calculating the total cost for each.
Algorithm
- Identify cells with more than one stone (sources) and cells with zero stones (destinations).
- Create a list
excessStonescontaining the coordinates of each extra stone. If a cell hasgstones, its coordinate is addedg-1times. - Create a list
emptyCellscontaining the coordinates of each empty cell. - Generate all possible permutations of the
emptyCellslist. - For each permutation, calculate the total cost by summing the Manhattan distances between the
i-th stone inexcessStonesand thei-th cell in the permutedemptyCellslist. - The minimum cost found among all permutations is the answer.
Walkthrough
The core idea is to perform a brute-force search over all possible pairings of surplus stones with empty cells.
-
Identify Sources and Destinations: First, we iterate through the 3x3 grid to find two types of cells:
- Cells with
grid[i][j] > 1: These are sources of extra stones. For each such cell, we add its coordinates(i, j)to a list calledexcessStonesexactlygrid[i][j] - 1times. - Cells with
grid[i][j] == 0: These are empty cells that need a stone. We add their coordinates(i, j)to a list calledemptyCells.
- Cells with
-
Generate Permutations: The problem is now to match each stone in
excessStonesto a unique cell inemptyCells. We can solve this by trying every possible permutation of theemptyCellslist. For each permutation, we pair the first excess stone with the first cell in the permuted list, the second stone with the second cell, and so on. -
Calculate Cost: For each complete pairing (permutation), we calculate the total moves required. The number of moves to transfer one stone from
(r1, c1)to(r2, c2)is the Manhattan distance:|r1 - r2| + |c1 - c2|. We sum these distances for all pairs in the current assignment. -
Find Minimum: We keep track of the minimum total moves found across all permutations. After checking all permutations, this minimum value is the solution.
import java.util.ArrayList; import java.util.Collections;import java.util.List; class Solution { private List<int[]> excessStones = new ArrayList<>(); private List<int[]> emptyCells = new ArrayList<>(); private int minMoves = Integer.MAX_VALUE; public int minimumMoves(int[][] grid) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (grid[i][j] > 1) { for (int k = 0; k < grid[i][j] - 1; k++) { excessStones.add(new int[]{i, j}); } } else if (grid[i][j] == 0) { emptyCells.add(new int[]{i, j}); } } } if (excessStones.isEmpty()) { return 0; } permute(0); return minMoves; } private void permute(int start) { if (start == emptyCells.size()) { int currentMoves = 0; for (int i = 0; i < excessStones.size(); i++) { currentMoves += Math.abs(excessStones.get(i)[0] - emptyCells.get(i)[0]) + Math.abs(excessStones.get(i)[1] - emptyCells.get(i)[1]); } minMoves = Math.min(minMoves, currentMoves); return; } for (int i = start; i < emptyCells.size(); i++) { Collections.swap(emptyCells, start, i); permute(start + 1); Collections.swap(emptyCells, start, i); // backtrack } }}Complexity
Time
O(k! * k), where k is the number of empty cells. The algorithm generates k! permutations of the `emptyCells` list. For each permutation, it takes O(k) time to calculate the total Manhattan distance. Given that k is at most 8, this is computationally feasible.
Space
O(k), where k is the number of empty cells. This space is used to store the `excessStones` and `emptyCells` lists, as well as for the recursion stack during permutation generation.
Trade-offs
Pros
Conceptually straightforward and relatively easy to implement.
Guaranteed to find the optimal solution because it exhaustively checks every possibility.
Cons
The factorial time complexity
O(k!)makes this approach impractical for slightly larger grids or more items to match.It is less efficient than dynamic programming, even for the small constraints of this problem.
This approach significantly optimizes the brute-force method by using dynamic programming with bitmasking. Instead of re-calculating the cost for the same subproblems, we store the results in a memoization table. A subproblem is defined by (stoneIndex, mask), representing the minimum moves to place the remaining stones (from stoneIndex onwards) into a specific set of available empty cells (represented by mask). This avoids the redundant computations inherent in the permutation approach and reduces the complexity from factorial to exponential, which is a substantial improvement.
Algorithm
- First, identify and create lists of
excessStonesandemptyCellsas in the previous approach. Let their size bek. - Define a recursive function,
solve(stoneIndex, mask), which calculates the minimum cost to place stones fromstoneIndextok-1into the available empty cells. - The
maskis a bitmask where thei-th bit being set indicates that thei-th empty cell is already occupied. - Use a 2D array,
memo[stoneIndex][mask], to store the results of subproblems to avoid redundant computations. - The base case for the recursion is when all stones are placed (
stoneIndex == k), which costs 0. - In the recursive step, iterate through all empty cells. If a cell is not yet occupied (checked via the mask), calculate the cost to move the current stone there and add it to the result of the recursive call for the next stone with an updated mask.
- The final answer is the result of the initial call
solve(0, 0).
Walkthrough
This method solves the assignment problem more efficiently by using recursion with memoization, a technique also known as top-down dynamic programming.
-
Identify Sources and Destinations: This step is the same as the brute-force approach. We populate the
excessStonesandemptyCellslists. -
Define State for DP: We define a recursive function that represents a state of the problem. A good state representation is
(stoneIndex, mask), wherestoneIndexis the index of the stone from theexcessStoneslist we are currently trying to place, andmaskis a bitmask representing the set of usedemptyCells. If thej-th bit ofmaskis 1, it means thej-th cell inemptyCellshas been filled. -
Recursive Relation with Memoization:
- The function
solve(stoneIndex, mask)will compute the minimum moves to place stones fromstoneIndexto the end of the list, given that the cells inmaskare already taken. - Base Case: If
stoneIndexreaches the size ofexcessStones, it means all stones have been placed, so we return 0. - Memoization: Before computing, we check if
memo[stoneIndex][mask]has already been calculated. If so, we return the stored value. - Recursion: We iterate through all empty cells
j. If thej-th cell is available ((mask & (1 << j)) == 0), we calculate the cost of movingexcessStones[stoneIndex]toemptyCells[j]. We then recursively callsolve(stoneIndex + 1, mask | (1 << j))to get the minimum cost for the rest of the stones. We take the minimum over all possible choices for the current stone. - The result is stored in
memo[stoneIndex][mask]before being returned.
- The function
-
Initial Call: The process starts by calling
solve(0, 0), which means we begin by placing the 0-th stone with no empty cells initially occupied.
import java.util.ArrayList;import java.util.List; class Solution { private List<int[]> excessStones = new ArrayList<>(); private List<int[]> emptyCells = new ArrayList<>(); private Integer[][] memo; public int minimumMoves(int[][] grid) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (grid[i][j] > 1) { for (int k = 0; k < grid[i][j] - 1; k++) { excessStones.add(new int[]{i, j}); } } else if (grid[i][j] == 0) { emptyCells.add(new int[]{i, j}); } } } if (excessStones.isEmpty()) { return 0; } int k = excessStones.size(); memo = new Integer[k][1 << k]; return solve(0, 0); } private int solve(int stoneIndex, int mask) { if (stoneIndex == excessStones.size()) { return 0; } if (memo[stoneIndex][mask] != null) { return memo[stoneIndex][mask]; } int minCost = Integer.MAX_VALUE; int[] source = excessStones.get(stoneIndex); for (int i = 0; i < emptyCells.size(); i++) { // Check if the i-th empty cell is NOT used if ((mask & (1 << i)) == 0) { int[] dest = emptyCells.get(i); int cost = Math.abs(source[0] - dest[0]) + Math.abs(source[1] - dest[1]); // Recurse for the next stone, marking the i-th cell as used int remainingCost = solve(stoneIndex + 1, mask | (1 << i)); if (remainingCost != Integer.MAX_VALUE) { minCost = Math.min(minCost, cost + remainingCost); } } } return memo[stoneIndex][mask] = minCost; }}Complexity
Time
O(k^2 * 2^k), where k is the number of empty cells. There are `k * 2^k` possible states for `(stoneIndex, mask)`. For each state, we iterate through `k` possible empty cells to find a match. This is very fast for `k <= 8`.
Space
O(k * 2^k), where k is the number of empty cells. This space is dominated by the memoization table. The recursion stack depth adds an additional O(k).
Trade-offs
Pros
Much more efficient than the brute-force permutation approach, with a significant reduction in time complexity.
Guaranteed to find the optimal solution.
It is a standard and powerful technique (DP on subsets) for solving assignment and matching problems with small numbers of items.
Cons
The time complexity is still exponential, making it unsuitable for problems where
kis large (e.g., > 20).Requires significantly more memory (
O(k * 2^k)) for the memoization table compared to the permutation approach.The logic involving bitmasking can be more complex to understand and implement correctly.
Solutions
Solution
class Solution {public int minimumMoves(int[][] grid) { Deque<String> q = new ArrayDeque<>(); q.add(f(grid)); Set<String> vis = new HashSet<>(); vis.add(f(grid)); int[] dirs = {-1, 0, 1, 0, -1}; for (int ans = 0;; ++ans) { for (int k = q.size(); k > 0; --k) { String p = q.poll(); if ("111111111".equals(p)) { return ans; } int[][] cur = g(p); for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { if (cur[i][j] > 1) { for (int d = 0; d < 4; ++d) { int x = i + dirs[d]; int y = j + dirs[d + 1]; if (x >= 0 && x < 3 && y >= 0 && y < 3 && cur[x][y] < 2) { int[][] nxt = new int[3][3]; for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { nxt[r][c] = cur[r][c]; } } nxt[i][j]--; nxt[x][y]++; String s = f(nxt); if (!vis.contains(s)) { vis.add(s); q.add(s); } } } } } } } } }private String f(int[][] grid) { StringBuilder sb = new StringBuilder(); for (int[] row : grid) { for (int x : row) { sb.append(x); } } return sb.toString(); }private int[][] g(String s) { int[][] grid = new int[3][3]; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { grid[i][j] = s.charAt(i * 3 + j) - '0'; } } return grid; }}class Solution {public: int minimumMoves(vector<vector<int>> &grid) { using pii = pair<int, int>; vector<pii> left, right; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { if (grid[i][j] == 0) { left.emplace_back(i, j); } else { for (int k = 1; k < grid[i][j]; ++k) { right.emplace_back(i, j); } } } } auto cal = [](pii a, pii b) { return abs(a.first - b.first) + abs(a.second - b.second); }; int n = left.size(); int f[1 << n]; memset(f, 0x3f, sizeof(f)); f[0] = 0; for (int i = 1; i < 1 << n; ++i) { int k = __builtin_popcount(i); for (int j = 0; j < n; ++j) { if (i >> j & 1) { f[i] = min(f[i], f[i ^ (1 << j)] + cal(left[k - 1], right[j])); } } } return f[(1 << n) - 1]; }};class Solution: def minimumMoves(self, grid: List[List[int]]) -> int: q = deque([tuple(tuple(row) for row in grid)]) vis = set(q) ans = 0 dirs = (- 1, 0, 1, 0, - 1) while 1: for _ in range(len(q)): cur = q . popleft() if all(x for row in cur for x in row): return ans for i in range(3): for j in range(3): if cur[i][j] > 1: for a, b in pairwise(dirs): x, y = i + a, j + b if 0 <= x < 3 and 0 <= y < 3 and cur[x][y] < 2: nxt = [list(row) for row in cur] nxt[i][j] -= 1 nxt[x][y] += 1 nxt = tuple(tuple(row) for row in nxt) if nxt not in vis: vis . add(nxt) q . append(nxt) ans += 1Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.