Digit Operations to Make Two Integers Equal
MedPrompt
You are given two integers n and m that consist of the same number of digits.
You can perform the following operations any number of times:
- Choose any digit from
nthat is not 9 and increase it by 1. - Choose any digit from
nthat is not 0 and decrease it by 1.
The integer n must not be a prime number at any point, including its original value and after each operation.
The cost of a transformation is the sum of all values that n takes throughout the operations performed.
Return the minimum cost to transform n into m. If it is impossible, return -1.
Example 1:
Input: n = 10, m = 12
Output: 85
Explanation:
We perform the following operations:
- Increase the first digit, now
n = 20. - Increase the second digit, now
n = 21. - Increase the second digit, now
n = 22. - Decrease the first digit, now
n = 12.
Example 2:
Input: n = 4, m = 8
Output: -1
Explanation:
It is impossible to make n equal to m.
Example 3:
Input: n = 6, m = 2
Output: -1
Explanation:
Since 2 is already a prime, we can't make n equal to m.
Constraints:
1 <= n, m < 104nandmconsist of the same number of digits.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a classic brute-force Depth First Search (DFS) to explore all possible valid transformation sequences from n to m. It recursively explores each path, calculates its total cost, and keeps track of the minimum cost found. To prevent infinite loops, it avoids visiting the same number twice within a single path.
Algorithm
- Pre-compute prime numbers up to 9999 using a sieve.
- Check if the initial
nor targetmare prime. If either is, a solution is impossible, so return -1. - Initialize a global variable
min_costto infinity. - Create a recursive function, let's call it
dfs(u, current_cost, path):u: the current number.current_cost: the sum of numbers in the path so far.path: a set of numbers visited in the current path to avoid cycles.
- The base case for the recursion is when
u == m. If so, updatemin_cost = min(min_cost, current_cost). - Add a pruning step: if
current_cost >= min_cost, stop exploring this path. - In the recursive step, generate all valid neighbors
vofu. - A neighbor
vis valid if it's reachable by one digit operation, is not prime, and is not in the currentpath. - For each valid neighbor
v, make a recursive call:dfs(v, current_cost + v, new_path_with_v). - Start the search by calling
dfs(n, n, {n}). - After the search is complete, if
min_costis still infinity, it meansmis unreachable. Otherwise,min_costholds the answer.
Walkthrough
The core of this method is a recursive function that explores paths starting from n. The function takes the current number, the accumulated cost, and the set of visited nodes in the current path as arguments.
When the function reaches the target m, it compares the path's cost with the global minimum and updates it if the new path is cheaper. To optimize slightly, the recursion is pruned if the current path's cost already exceeds the best-known minimum cost.
However, because the number of possible paths can be enormous, this method is not practical for the given constraints. It explores paths one by one without prioritizing potentially cheaper paths, leading to a very large search space. For example, it might explore a very long and expensive path completely before trying a short and cheap one.
Here is a conceptual outline of the DFS function:
long minCost = Long.MAX_VALUE;boolean[] isPrime; void solve(int n, int m) { // Sieve pre-computation sieve(9999); if (isPrime[n] || isPrime[m]) { // return -1; return; } Set<Integer> path = new HashSet<>(); path.add(n); dfs(n, m, n, path); // return minCost == Long.MAX_VALUE ? -1 : minCost;} void dfs(int u, int m, long currentCost, Set<Integer> path) { if (currentCost >= minCost) { return; // Pruning } if (u == m) { minCost = Math.min(minCost, currentCost); return; } for (int v : getNeighbors(u)) { // getNeighbors generates valid next numbers if (!path.contains(v)) { path.add(v); dfs(v, m, currentCost + v, path); path.remove(v); // Backtrack } }}Complexity
Time
O(B^D), where B is the branching factor (up to 8 neighbors per number) and D is the maximum path depth. This is exponential and far too slow for the given constraints.
Space
O(V), where V is the number of non-prime integers up to 9999. The recursion depth and the `path` set can, in the worst case, include all possible nodes.
Trade-offs
Pros
Conceptually simple and relatively easy to understand for those familiar with recursion.
Cons
Extremely inefficient due to exploring a potentially exponential number of paths.
Almost certain to fail with a 'Time Limit Exceeded' error for the given constraints.
State management is complex to avoid cycles while allowing nodes to be revisited via different, potentially cheaper, paths.
This problem can be efficiently solved by modeling it as a shortest path problem on a graph. The integers from 1 to 9999 are the graph's nodes. A directed edge exists from number u to v if v can be reached from u by a single valid digit operation. The cost of traversing this edge is defined in a way that accumulates the values of the nodes. We can then use a modified version of Dijkstra's algorithm to find the path from n to m with the minimum cumulative cost.
Algorithm
- Special Case: If
n == m, no operations are needed, so the cost is 0. Return 0. - Preprocessing: Generate all prime numbers up to 9999 using the Sieve of Eratosthenes. This allows for O(1) primality checks.
- Initial Checks: If
normis prime, a valid transformation is impossible. Return -1. - Graph Model: Treat the integers as nodes in a graph. A directed edge exists from
utovifvcan be obtained fromuvia a single valid digit operation. - Dijkstra's Algorithm Setup:
- The cost of a path
v_0, v_1, ..., v_kisv_0 + v_1 + ... + v_k. - The cost to reach a neighbor
vfromuisdist[u] + v, wheredist[u]is the minimum cost to reachu. - Initialize a
distarray with infinity for all nodes, and setdist[n] = n. - Use a priority queue to store pairs of
(cost, node), ordered by cost.
- The cost of a path
- Execution:
- Add
(n, n)to the priority queue. - While the priority queue is not empty, extract the node
uwith the smallest cost. - If
uis the targetm,dist[m]contains the minimum total cost. Returndist[m]. - For each valid neighbor
vofu(not prime, same number of digits):- If
dist[u] + v < dist[v], updatedist[v] = dist[u] + vand add(dist[v], v)to the priority queue.
- If
- Add
- No Path: If the loop finishes before
mis reached,mis unreachable. Return -1.
Walkthrough
The key insight is to recognize this as a shortest path problem where the edge weights are not constant but depend on the destination node. The total cost of a path is the sum of the values of all nodes in it, including the start and end nodes. A Dijkstra-like algorithm is perfect for this.
Let dist[i] be the minimum cost to transform n into i. The cost is the sum of all numbers on the path. We initialize dist[n] = n and all other dist values to infinity. We use a priority queue to always explore the node with the current minimum accumulated cost.
When we are at a node u and consider moving to a neighbor v, the cost of the new path to v is dist[u] + v. If this is less than the currently known dist[v], we update dist[v] and add v to the priority queue. This process guarantees that when we first extract m from the priority queue, we have found the minimum cost path to it.
Initial checks for primality of n and m are crucial, as is handling the n == m case separately.
import java.util.*; class Solution { private boolean[] isPrime; private int numDigits; private void sieve(int max) { isPrime = new boolean[max + 1]; Arrays.fill(isPrime, true); isPrime[0] = isPrime[1] = false; for (int p = 2; p * p <= max; p++) { if (isPrime[p]) { for (int i = p * p; i <= max; i += p) { isPrime[i] = false; } } } } private List<Integer> getNeighbors(int u) { List<Integer> neighbors = new ArrayList<>(); char[] digits = String.valueOf(u).toCharArray(); for (int i = 0; i < digits.length; i++) { char originalDigit = digits[i]; // Increase digit if (originalDigit < '9') { digits[i]++; int v = Integer.parseInt(new String(digits)); if (!isPrime[v]) { neighbors.add(v); } } digits[i] = originalDigit; // Backtrack // Decrease digit if (originalDigit > '0') { if (i == 0 && numDigits > 1 && originalDigit == '1') { // This would change the number of digits (e.g., 10 -> 0), so skip. } else { digits[i]--; int v = Integer.parseInt(new String(digits)); if (!isPrime[v]) { neighbors.add(v); } } } digits[i] = originalDigit; // Backtrack } return neighbors; } public int minimumCost(int n, int m) { if (n == m) { return 0; } sieve(9999); if (isPrime[n] || isPrime[m]) { return -1; } numDigits = String.valueOf(n).length(); long[] dist = new long[10000]; Arrays.fill(dist, Long.MAX_VALUE); dist[n] = n; // Priority Queue stores {cost, number} PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(a -> a[0])); pq.offer(new long[]{n, n}); while (!pq.isEmpty()) { long[] current = pq.poll(); long currentCost = current[0]; int u = (int) current[1]; if (currentCost > dist[u]) { continue; } if (u == m) { return (int) dist[m]; } for (int v : getNeighbors(u)) { if (dist[u] != Long.MAX_VALUE && dist[u] + v < dist[v]) { dist[v] = dist[u] + v; pq.offer(new long[]{dist[v], v}); } } } return -1; }}Complexity
Time
O(D * V * log(V)), where V is the number of nodes (10000) and D is the number of digits (at most 4). For each node, we do O(D) work to find neighbors, and there are O(D*V) edges in total. Dijkstra with a priority queue gives this complexity.
Space
O(V), where V is the number of nodes (10000). This is for the `dist` array, the `isPrime` sieve array, and the priority queue, which can hold up to V elements in the worst case.
Trade-offs
Pros
Guaranteed to find the optimal solution (minimum cost).
Efficient and will pass within the time limits for the given constraints.
Cons
More complex to implement than a simple DFS/BFS.
Requires careful handling of the non-standard cost definition, which dictates the relaxation step in the algorithm.
Solutions
Solution
class Solution {private boolean[] sieve;private void runSieve() { sieve = new boolean[100000]; Arrays.fill(sieve, true); sieve[0] = false; sieve[1] = false; for (int i = 2; i < 100000; i++) { if (sieve[i]) { for (int j = 2 * i; j < 100000; j += i) { sieve[j] = false; } } } }private int solve(int n, int m) { PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a->a[0])); pq.add(new int[]{n, n}); Set<Integer> visited = new HashSet<>(); while (!pq.isEmpty()) { int[] top = pq.poll(); int sum = top[0], cur = top[1]; if (visited.contains(cur)) { continue; } visited.add(cur); if (cur == m) { return sum; } char[] s = String.valueOf(cur).toCharArray(); for (int i = 0; i < s.length; i++) { char c = s[i]; if (s[i] < '9') { s[i] = (char)(s[i] + 1); int next = Integer.parseInt(new String(s)); if (!sieve[next] && !visited.contains(next)) { pq.add(new int[]{sum + next, next}); } s[i] = c; } if (s[i] > '0' && !(i == 0 && s[i] == '1')) { s[i] = (char)(s[i] - 1); int next = Integer.parseInt(new String(s)); if (!sieve[next] && !visited.contains(next)) { pq.add(new int[]{sum + next, next}); } s[i] = c; } } } return -1; }public int minOperations(int n, int m) { runSieve(); if (sieve[n] || sieve[m]) { return -1; } return solve(n, m); }}class Solution {private: vector<bool> sieve; void runSieve() { sieve.resize(100000, true); sieve[0] = false, sieve[1] = false; for (int i = 2; i < 1e5; ++i) { if (sieve[i]) { for (int j = 2 * i; j < 1e5; j += i) { sieve[j] = false; } } } } int solve(int n, int m) { priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; unordered_set<int> vis; pq.push({n, n}); while (!pq.empty()) { int sum = pq.top().first, cur = pq.top().second; pq.pop(); if (vis.find(cur) != vis.end()) continue; vis.insert(cur); if (cur == m) return sum; string s = to_string(cur); for (int i = 0; i < s.size(); ++i) { char c = s[i]; if (s[i] < '9') { s[i]++; int next = stoi(s); if (!sieve[next] && vis.find(next) == vis.end()) { pq.push({sum + next, next}); } s[i] = c; } if (s[i] > '0' && !(i == 0 && s[i] == '1')) { s[i]--; int next = stoi(s); if (!sieve[next] && vis.find(next) == vis.end()) { pq.push({sum + next, next}); } s[i] = c; } } } return -1; } public: int minOperations(int n, int m) { runSieve(); if (sieve[n] || sieve[m]) return -1; return solve(n, m); }};import heapq class Solution : def __init__ ( self ): self . sieve = [] def run_sieve ( self ): self . sieve = [ True ] * 100000 self . sieve [ 0 ], self . sieve [ 1 ] = False , False for i in range ( 2 , 100000 ): if self . sieve [ i ]: for j in range ( 2 * i , 100000 , i ): self . sieve [ j ] = False def solve ( self , n , m ): pq = [] heapq . heappush ( pq , ( n , n )) visited = set () while pq : sum_ , cur = heapq . heappop ( pq ) if cur in visited : continue visited . add ( cur ) if cur == m : return sum_ s = list ( str ( cur )) for i in range ( len ( s )): c = s [ i ] if s [ i ] < '9' : s [ i ] = chr ( ord ( s [ i ]) + 1 ) next_ = int ( '' . join ( s )) if not self . sieve [ next_ ] and next_ not in visited : heapq . heappush ( pq , ( sum_ + next_ , next_ )) s [ i ] = c if s [ i ] > '0' and not ( i == 0 and s [ i ] == '1' ): s [ i ] = chr ( ord ( s [ i ]) - 1 ) next_ = int ( '' . join ( s )) if not self . sieve [ next_ ] and next_ not in visited : heapq . heappush ( pq , ( sum_ + next_ , next_ )) s [ i ] = c return - 1 def minOperations ( self , n , m ): self . run_sieve () if self . sieve [ n ] or self . sieve [ m ]: return - 1 return self . solve ( n , m )Video 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.