LeetCode 1409 - Queries on a Permutation With Key
This problem asks us to simulate a dynamic permutation of integers and answer position queries as elements continuously
Difficulty: 🟡 Medium
Topics: Array, Binary Indexed Tree, Simulation
Solution
Problem Understanding
This problem asks us to simulate a dynamic permutation of integers and answer position queries as elements continuously move.
Initially, we are given a permutation:
P = [1, 2, 3, ..., m]
For every value in the queries array, we must perform two operations:
- Find the current position of
queries[i]in the permutationP, using 0-based indexing. - Move that queried value to the front of the permutation.
The position found before moving the element becomes part of the result array.
In other words, every query changes the arrangement of the permutation, and future queries depend on the updated order. This means the permutation evolves over time, so we cannot process each query independently.
For example, if:
P = [1,2,3,4,5]
queries[i] = 3
The value 3 is currently at index 2, so we record 2 in the answer. Then we move 3 to the front:
P = [3,1,2,4,5]
The next query works on this modified permutation.
The input consists of:
queries, an array of integers representing the requested valuesm, which defines the initial permutation[1,2,3,...,m]
The output is an array where each element represents the position of the corresponding query value before it was moved to the front.
The constraints are relatively small:
m <= 1000queries.length <= m <= 1000
These limits tell us that even a quadratic solution may be acceptable. Since the maximum work is around 1000 × 1000 = 10^6, brute force is feasible. However, the problem is tagged with Binary Indexed Tree, which suggests there is a more scalable and elegant approach.
Several edge cases are important to consider. Repeated queries can move the same number to the front multiple times, which means future accesses may suddenly become index 0. Queries may also repeatedly target values near the end of the permutation, forcing many shifts in a naive implementation. The problem guarantees all query values are valid integers between 1 and m, so we never need to handle missing elements or invalid inputs.
Approaches
Brute Force Simulation
The most straightforward solution is to directly simulate the permutation.
We maintain a list representing the current permutation. For every query:
- Search for the queried value using a linear scan.
- Record its index.
- Remove it from its current position.
- Insert it at the beginning.
This works because it exactly follows the rules of the problem. Every query updates the permutation correctly, so later queries observe the proper ordering.
However, locating an element takes O(m) time, and removing plus inserting into a list also costs O(m) because elements shift positions. Since we process every query this way, the total complexity becomes O(n × m) where n = len(queries).
Although acceptable under the current constraints, this method becomes inefficient for larger inputs.
Optimal Approach, Binary Indexed Tree + Position Tracking
The key observation is that we repeatedly need to answer this question:
"How many elements appear before a given number?"
This is a classic prefix sum problem.
Instead of physically moving elements in an array, we can assign every number a position index and maintain a structure that tracks which positions are occupied.
We use:
- A hash map / array to store the current position of every number.
- A Binary Indexed Tree (Fenwick Tree) to efficiently count how many elements exist before a position.
The trick is to reserve space at the front for future moves.
Suppose there are n queries.
We initially place numbers at positions:
n+1, n+2, ..., n+m
The first n positions remain empty.
Whenever a queried value appears:
- Use the Binary Indexed Tree to count how many active elements exist before its position.
- Remove the number from its old position.
- Move it to the next free front position.
- Update its stored position.
This avoids expensive shifting operations entirely.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n × m) | O(m) | Direct simulation of the permutation |
| Optimal | O((n + m) log(n + m)) | O(n + m) | Uses Binary Indexed Tree for efficient position queries |
Algorithm Walkthrough
- Let
n = len(queries)and create a Binary Indexed Tree of sizen + m.
We allocate extra space because queried elements will move to the front. Instead of shifting values, we assign them new smaller indices.
2. Store the initial position of every value from 1 to m.
Initially, value x is placed at:
position[x] = n + x
We intentionally leave positions [1...n] empty for future front insertions.
3. Mark all initial positions as occupied in the Binary Indexed Tree.
Every active number contributes 1 at its current position.
4. Maintain a variable called front_position.
Start with:
front_position = n
This represents the next available slot at the front. 5. Process each query one at a time.
For the queried value x:
- Get its current position from
position[x]. - Query the Binary Indexed Tree for the count of elements before it:
rank = prefix_sum(current_position) - 1
We subtract 1 because the prefix sum includes the element itself, but we only want elements before it.
3. Add this rank to the result array.
4. Remove the element from its old position by subtracting 1 in the Binary Indexed Tree.
5. Move it to front_position by adding 1 there.
6. Update position[x].
7. Decrement front_position.
6. Return the result array after processing all queries.
Why it works
The Binary Indexed Tree always maintains an accurate count of which positions currently contain elements. Since every value has a unique position, the prefix sum up to an element's location equals the number of active elements before or at that position. Subtracting one gives the exact 0-based index of that value in the current permutation. Updating positions after every query preserves the correct ordering invariant, ensuring all future queries observe the updated permutation.
Python Solution
from typing import List
class Solution:
def processQueries(self, queries: List[int], m: int) -> List[int]:
n = len(queries)
size = n + m
# Fenwick Tree
fenwick = [0] * (size + 1)
def update(index: int, delta: int) -> None:
while index <= size:
fenwick[index] += delta
index += index & -index
def query(index: int) -> int:
total = 0
while index > 0:
total += fenwick[index]
index -= index & -index
return total
# Store current positions of numbers
positions = [0] * (m + 1)
# Initialize permutation positions
for value in range(1, m + 1):
position = n + value
positions[value] = position
update(position, 1)
result = []
front_position = n
for value in queries:
current_position = positions[value]
# Find 0-based index
index = query(current_position) - 1
result.append(index)
# Remove old position
update(current_position, -1)
# Move to front
positions[value] = front_position
update(front_position, 1)
front_position -= 1
return result
The implementation starts by creating a Fenwick Tree, which efficiently supports prefix sum operations and point updates in logarithmic time. The update() helper adjusts the count at a specific position, while query() computes how many active elements exist up to a given position.
The positions array stores the current location of each number. Instead of placing elements directly at the front of an array, every value initially starts in the range [n+1, n+m], leaving room for future front insertions.
For every query, the code retrieves the current position of the queried value and computes its rank using a Fenwick prefix sum. Since the queried element contributes to its own prefix sum, subtracting one converts the result into the required 0-based index.
After recording the answer, the implementation removes the element from its old location, inserts it into the current front slot, updates its stored position, and moves the front pointer leftward for the next query.
Go Solution
func processQueries(queries []int, m int) []int {
n := len(queries)
size := n + m
// Fenwick Tree
fenwick := make([]int, size+1)
update := func(index int, delta int) {
for index <= size {
fenwick[index] += delta
index += index & -index
}
}
query := func(index int) int {
sum := 0
for index > 0 {
sum += fenwick[index]
index -= index & -index
}
return sum
}
// Current positions of each value
positions := make([]int, m+1)
// Initialize positions
for value := 1; value <= m; value++ {
position := n + value
positions[value] = position
update(position, 1)
}
result := make([]int, 0, n)
frontPosition := n
for _, value := range queries {
currentPosition := positions[value]
// Get 0-based index
index := query(currentPosition) - 1
result = append(result, index)
// Remove from old position
update(currentPosition, -1)
// Move to front
positions[value] = frontPosition
update(frontPosition, 1)
frontPosition--
}
return result
}
The Go implementation follows the same logic as the Python solution. Since Go does not support nested named functions as naturally as Python, the Fenwick Tree helpers are implemented as closures. Slices are used for dynamic storage of both the Fenwick Tree and positions.
No special handling for nil versus empty slices is needed because the problem guarantees valid input sizes. Integer overflow is also not a concern because the constraints are very small and all values comfortably fit within Go's int type.
Worked Examples
Example 1
queries = [3,1,2,1]
m = 5
n = 4
Initial positions:
| Value | Position |
|---|---|
| 1 | 5 |
| 2 | 6 |
| 3 | 7 |
| 4 | 8 |
| 5 | 9 |
Front position starts at 4.
| Query | Current Position | Prefix Sum | Result Index | New Position | Front Position After |
|---|---|---|---|---|---|
| 3 | 7 | 3 | 2 | 4 | 3 |
| 1 | 5 | 2 | 1 | 3 | 2 |
| 2 | 6 | 3 | 2 | 2 | 1 |
| 1 | 3 | 2 | 1 | 1 | 0 |
Final answer:
[2,1,2,1]
Example 2
queries = [4,1,2,2]
m = 4
n = 4
Initial positions:
| Value | Position |
|---|---|
| 1 | 5 |
| 2 | 6 |
| 3 | 7 |
| 4 | 8 |
| Query | Current Position | Prefix Sum | Result Index | New Position |
|---|---|---|---|---|
| 4 | 8 | 4 | 3 | 4 |
| 1 | 5 | 2 | 1 | 3 |
| 2 | 6 | 3 | 2 | 2 |
| 2 | 2 | 1 | 0 | 1 |
Final answer:
[3,1,2,0]
Example 3
queries = [7,5,5,8,3]
m = 8
n = 5
Initial positions:
1→6, 2→7, 3→8, 4→9, 5→10, 6→11, 7→12, 8→13
| Query | Current Position | Result Index | New Position |
|---|---|---|---|
| 7 | 12 | 6 | 5 |
| 5 | 10 | 5 | 4 |
| 5 | 4 | 0 | 3 |
| 8 | 13 | 7 | 2 |
| 3 | 8 | 5 | 1 |
Final answer:
[6,5,0,7,5]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O((n + m) log(n + m)) | Every query performs Fenwick updates and prefix sums |
| Space | O(n + m) | Extra storage for Fenwick Tree and position mapping |
The initialization phase inserts m elements into the Binary Indexed Tree, costing O(m log(n+m)). Each query performs one prefix sum and two updates, each requiring O(log(n+m)) time. Since there are n queries, the total runtime becomes O((n + m) log(n + m)).
The extra memory comes from the Fenwick Tree of size n + m and the position array storing current locations of values 1 through m.
Test Cases
class Solution:
def processQueries(self, queries, m):
n = len(queries)
size = n + m
fenwick = [0] * (size + 1)
def update(index, delta):
while index <= size:
fenwick[index] += delta
index += index & -index
def query(index):
total = 0
while index > 0:
total += fenwick[index]
index -= index & -index
return total
positions = [0] * (m + 1)
for value in range(1, m + 1):
position = n + value
positions[value] = position
update(position, 1)
result = []
front_position = n
for value in queries:
current_position = positions[value]
index = query(current_position) - 1
result.append(index)
update(current_position, -1)
positions[value] = front_position
update(front_position, 1)
front_position -= 1
return result
solution = Solution()
assert solution.processQueries([3, 1, 2, 1], 5) == [2, 1, 2, 1] # Example 1
assert solution.processQueries([4, 1, 2, 2], 4) == [3, 1, 2, 0] # Example 2
assert solution.processQueries([7, 5, 5, 8, 3], 8) == [6, 5, 0, 7, 5] # Example 3
assert solution.processQueries([1], 1) == [0] # Minimum input size
assert solution.processQueries([1, 1, 1], 3) == [0, 0, 0] # Same element repeatedly
assert solution.processQueries([3, 3, 3], 3) == [2, 0, 0] # Element moves to front
assert solution.processQueries([5, 4, 3, 2, 1], 5) == [4, 4, 4, 4, 4] # Reverse access order
assert solution.processQueries([1, 2, 3, 4, 5], 5) == [0, 1, 2, 3, 4] # Already front sequence
assert solution.processQueries([2, 1, 2, 1], 2) == [1, 1, 1, 1] # Small repeated permutation
Test Summary
| Test | Why |
|---|---|
[3,1,2,1], 5 |
Verifies Example 1 |
[4,1,2,2], 4 |
Verifies Example 2 |
[7,5,5,8,3], 8 |
Verifies Example 3 |
[1], 1 |
Minimum valid input |
[1,1,1], 3 |
Repeated querying of front element |
[3,3,3], 3 |
Ensures moved element remains at front |
[5,4,3,2,1], 5 |
Stress test for back-to-front movement |
[1,2,3,4,5], 5 |
Sequential access pattern |
[2,1,2,1], 2 |
Small repeated interactions |
Edge Cases
Repeated Queries to the Same Value
A common source of bugs is repeatedly querying the same element. After the first query, the element should already be at the front, meaning all later accesses should return index 0.
For example:
queries = [3,3,3]
m = 3
After the first query, 3 moves to the front. The implementation correctly updates its position in the positions array, so subsequent prefix sum calculations return 0.
Minimum Input Size
The smallest valid case is:
queries = [1]
m = 1
Since the permutation is [1], the only value already exists at index 0. The implementation handles this correctly because the Fenwick Tree still supports a single position and the prefix sum logic remains valid.
Values Near the End of the Permutation
Queries may repeatedly target elements near the back of the current permutation, which could be expensive in a naive shifting solution.
For example:
queries = [5,4,3,2,1]
m = 5
A brute force approach repeatedly shifts many elements. The Binary Indexed Tree solution avoids physical movement entirely by updating position mappings and occupancy counts, maintaining logarithmic performance for every query.