Topics › Graphs › Shortest Path Algorithms and Problems
Minimum multiplications to reach end
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array arr of multipliers and two numbers start and end. In one step you may pick any arr[i] and replace start with (start * arr[i]) % 100000. Return the minimum number of steps needed to turn start into end, or -1 if it is impossible. Multipliers can be reused any number of times.
Constraints (GeeksforGeeks): 1 ≤ arr.length ≤ 10^4, 1 ≤ arr[i] ≤ 10^5, 1 ≤ start, end < 10^5.
Example 1
arr = [2, 5, 7], start = 3, end = 30
answer = 2 // 3 * 2 = 6, then 6 * 5 = 30
Example 2
arr = [3, 4, 65], start = 7, end = 66175
answer = 4 // 7 -> 21 -> 63 -> 4095 -> 266175 % 100000 = 66175
At first sight this looks like a number puzzle, but the modulo makes it a graph problem. Every value is one of the residues 0 to 99999, so there are exactly 100000 possible states. From a state x there is an edge to (x * m) % 100000 for every multiplier m. Each step costs one, so the question is the length of the shortest path from start to end in that implicit graph. We never build the graph: the neighbours of x are computed on the fly.
Approach 1: Dijkstra on the residues
Intuition. This sits in the shortest-path section of the sheet, and the most general tool is Dijkstra: keep dist[x], the fewest steps known to reach residue x, and repeatedly expand the unexpanded residue with the smallest dist from a min-heap. When end comes out of the heap, its distance is final, because every other entry in the heap is at least as far away and steps are never negative.
Two details matter. First, x * m can be as large as about 10^5 × 10^5 = 10^10, which overflows a 32-bit int, so the product is computed in long before the modulo. Second, the state space is bounded at 100000, so the search always terminates: if the heap empties without reaching end, the answer is -1.
Algorithm.
dist[start] = 0, everything else infinity; push{0, start}.- Pop the entry with the fewest steps. If its value is
end, return its steps. If it is stale, skip it. - For each multiplier
m, computenext = value * m % 100000. Ifsteps + 1 < dist[next], update it and push{steps + 1, next}. - Return
-1when the heap is empty.
static int dijkstra(int[] arr, int start, int end) {
int[] dist = new int[MOD];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
pq.offer(new int[]{0, start}); // {steps, value}
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int steps = cur[0], value = cur[1];
if (value == end) return steps; // first time end leaves the heap, its distance is final
if (steps > dist[value]) continue; // stale entry
for (int m : arr) {
int next = (int) ((long) value * m % MOD); // long: value * m can reach about 10^10
if (steps + 1 < dist[next]) {
dist[next] = steps + 1;
pq.offer(new int[]{steps + 1, next});
}
}
}
return -1;
}
Complexity. Each of the 100000 residues is expanded at most once, and each expansion tries all m multipliers, so there are at most 100000 · m relaxations. Each costs a heap operation, giving O(100000 · m · log(100000 · m)) time in the worst case. Space O(100000) for dist, plus the heap.
That works, but the heap is doing a job nobody asked for. Every edge has the same weight.
Approach 2: Optimal, plain BFS
Intuition. When all edges cost exactly one, a FIFO queue already pops states in non-decreasing order of steps. Everything at distance d enters the queue before anything at distance d + 1, because a state at distance d + 1 can only be discovered while expanding a state at distance d. So the queue does what the heap did, without the logarithm. This is the “Dijkstra with a queue” form Striver uses for this problem.
With BFS, the first time a residue is discovered is already its shortest distance. So instead of comparing steps + 1 < dist[next] we only need “has next been seen?”, and we can return the moment end is discovered rather than when it is popped.
Algorithm.
- If
start == end, return0. dist[start] = 0, every other entry-1(unseen); enqueuestart.- Dequeue
value. For every multiplierm:next = value * m % 100000. Ifnextis unseen, setdist[next] = dist[value] + 1; return it ifnext == end; otherwise enqueue it. - Return
-1when the queue is empty.
static int bfs(int[] arr, int start, int end) {
if (start == end) return 0;
int[] dist = new int[MOD];
Arrays.fill(dist, -1); // -1 = not reached yet
dist[start] = 0;
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(start);
while (!queue.isEmpty()) {
int value = queue.poll();
for (int m : arr) {
int next = (int) ((long) value * m % MOD);
if (dist[next] == -1) {
dist[next] = dist[value] + 1;
if (next == end) return dist[next]; // the first discovery is the shortest
queue.offer(next);
}
}
}
return -1;
}
Dry run on Example 1 (arr = [2, 5, 7], start = 3, end = 30):
| dequeued | dist | new residues discovered |
|---|---|---|
| 3 | 0 | 6, 15, 21 (distance 1) |
| 6 | 1 | 12, 30 (distance 2): 30 is the end, return 2 |
6 * 7 = 42 is never reached because we return as soon as 30 appears.
Complexity. Time O(100000 · m): each residue is enqueued at most once and tries every multiplier. Space O(100000) for dist and the queue. The bound depends on the modulus, not on how large the numbers would get without it, which is the whole point of seeing it as a graph.
Why a bounded state space guarantees termination
Without the modulo, repeated multiplication would produce ever larger numbers and a search could run forever. With it, there are only 100000 distinct values, and each one is processed at most once, so both searches stop. This also explains the -1 cases. With arr = [2] and start = 1, every reachable value after the first step is even, so an odd end such as 3 is never discovered; the queue drains after visiting the 2505 distinct powers of two modulo 100000, and we return -1.
The same pattern appears in many puzzles: “lock combinations”, “minimum operations to turn one number into another”, “word ladder”. Whenever the values that can appear form a finite, small set and every move costs the same, model values as nodes and moves as edges, then run BFS.
Edge cases and pitfalls
- Start equals end. Answer
0, with no multiplications. The BFS checks it up front; Dijkstra returns it on the first pop. - Overflow.
99999 * 99999 = 9999800001does not fit in anint. The tests use exactly this case, where the correct answer is1because the product is1modulo100000. - Unreachable end. Return
-1. With multiplier1the value never changes; with only even multipliers odd residues are lost forever. - Residue zero. A multiplier of
100000(or a product divisible by it) sends the value to0, and0stays0. That is fine ifendis0; the tests cover it. - Duplicate multipliers. They only create parallel edges; the
seencheck means they cost a little time but never change the answer. - Using a
HashSetinstead of an array. It works, but anint[100000]is faster and simpler because the state space is small and dense.
Related problems
- Shortest path in undirected graph with unit weights is the same BFS on an explicit graph.
- Djisktra’s Algorithm is Approach 1’s general tool.
- Word ladder I is another implicit graph where nodes are generated on the fly.
- Shortest Distance in a Binary Maze uses the same “queue is enough for unit weights” argument.