Facebook Pixel

3930. Power Update After K-th Largest Insertion II πŸ”’

LeetCode β†—

Problem Description

You are given an integer array nums and an integer p.

You are also given a 2D integer array queries, where each queries[i] = [val_i, k_i].

For each query, you need to perform the following operations in order:

  • Insert val_i into nums.
  • Let x be the k_i-th largest element in the current nums (after the insertion).
  • Update p to p^x % (10^9 + 7).

Your task is to return an array ans where ans[i] represents the value of p after processing the i-th query.

In other words, the array nums grows by one element with each query, and after each insertion, you find the k_i-th largest element in the updated array, raise the current value of p to that power (taking the result modulo 10^9 + 7), and record this new value of p. Each query builds on the result of the previous one, since p is continuously updated throughout the process.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Ordered Set / Fenwick / Segment Tree?

This problem maps to Ordered Set / Fenwick / Segment Tree through a short path in the full flowchart.

Sortedinput ormonotonicyesDynamicrangequeries?yesOrdered Set /Fenwick /Segment Tree

Range queries with point updates require a segment tree for efficient answers.

Open in Flowchart

Intuition

The core challenge of this problem is that nums keeps changing β€” with each query, we insert a new element, and then we need to quickly find the k-th largest element in the updated array.

A naive approach would be to insert the value and then sort the array (or scan it) every time we need to find the k-th largest element. However, sorting after each insertion is wasteful, since the array is already mostly sorted except for the one newly inserted value.

This observation leads us to a key idea: if we keep the array sorted at all times, then inserting a new element and querying the k-th largest element both become efficient operations. Specifically, if the array is kept in ascending order, the k-th largest element is simply the element at index -k (counting from the end).

To maintain a sorted collection that supports fast insertion while preserving order, we use a sorted list data structure. Inserting an element into a sorted list takes about O(log n) time to find the position, and accessing any element by index is direct. This handles both the insertion step and the k-th largest lookup neatly.

The other part of each query is updating p to p^x % (10^9 + 7). Since x can be large, we rely on fast exponentiation (modular exponentiation) to compute the power efficiently in O(log x) time, while keeping the result within the modulus to avoid overflow. In Python, the built-in pow(p, x, mod) handles this directly.

Putting these ideas together: for each query, we add val to the sorted list, read off the k-th largest element using index -k, update p with modular exponentiation, and append the result to the answer array.

Pattern Learn more about Segment Tree, Math and Sorting patterns.

Solution Approach

Solution 1: Sorted List

We use a sorted list sl to maintain the current array nums in ascending order. By keeping the elements sorted at all times, we can efficiently insert new values and look up the k-th largest element.

The implementation proceeds as follows:

  1. Initialization: We create a sorted list sl from the initial nums array. We also prepare an empty answer array ans and define the modulus mod = 10^9 + 7.

  2. Processing each query: For each query [val, k]:

    • Insert val into the sorted list using sl.add(val). The sorted list automatically places val in its correct position to keep the list ordered, in O(log n) time.
    • Find the k-th largest element: Since sl is sorted in ascending order, the k-th largest element is at index -k, accessed directly as sl[-k].
    • Update p: We compute p = pow(p, sl[-k], mod) using fast exponentiation. The built-in pow(base, exp, mod) efficiently computes base^exp % mod in O(log exp) time without overflow.
    • Record the result: We append the updated p to the answer array ans.
  3. Return: After all queries are processed, we return ans.

The reference code captures this logic concisely:

class Solution:
    def powerUpdate(
        self, nums: list[int], p: int, queries: list[list[int]]
    ) -> list[int]:
        ans = []
        sl = SortedList(nums)
        mod = 10**9 + 7
        for val, k in queries:
            sl.add(val)
            p = pow(p, sl[-k], mod)
            ans.append(p)
        return ans

Complexity Analysis:

  • Let n be the length of nums and q be the number of queries.
  • Time Complexity: O((n + q) Γ— log(n + q) + q Γ— log x), where each insertion into the sorted list costs O(log(n + q)) and each modular exponentiation costs O(log x) with x being the exponent value.
  • Space Complexity: O(n + q) for storing all elements in the sorted list, plus the answer array.

Example Walkthrough

Let's trace through a small example to see the solution approach in action.

Input:

  • nums = [2, 3]
  • p = 2
  • queries = [[5, 2], [1, 3]]

We initialize a sorted list sl = [2, 3] and set mod = 10^9 + 7. The answer array ans starts empty.


Query 1: [val = 5, k = 2]

  1. Insert val = 5 into the sorted list.

    • sl.add(5) places 5 in its correct position.
    • Now sl = [2, 3, 5] (still sorted in ascending order).
  2. Find the k-th largest element (k = 2).

    • Since sl is sorted ascending, the 2nd largest is at index -2.
    • sl[-2] = 3. (The largest is 5 at index -1, the 2nd largest is 3 at index -2.)
  3. Update p.

    • p = pow(p, 3, mod) = pow(2, 3, mod) = 8.
  4. Record the result.

    • ans = [8].

Query 2: [val = 1, k = 3]

  1. Insert val = 1 into the sorted list.

    • sl.add(1) places 1 at the front.
    • Now sl = [1, 2, 3, 5].
  2. Find the k-th largest element (k = 3).

    • The 3rd largest is at index -3.
    • sl[-3] = 2. (Largest 5 at -1, 2nd largest 3 at -2, 3rd largest 2 at -3.)
  3. Update p.

    • p = pow(p, 2, mod) = pow(8, 2, mod) = 64.
  4. Record the result.

    • ans = [8, 64].

Final Result: ans = [8, 64]


Key takeaways from this walkthrough:

  • The sorted list keeps nums ordered after every insertion, so no re-sorting is needed.
  • Accessing the k-th largest element is a direct lookup at index -k, avoiding any scan.
  • Each query builds on the previous value of p β€” notice how p = 8 from Query 1 became the base for Query 2 (8^2 = 64), illustrating the chained nature of the updates.

Solution Implementation

1from sortedcontainers import SortedList
2
3
4class Solution:
5    def powerUpdate(
6        self, nums: list[int], p: int, queries: list[list[int]]
7    ) -> list[int]:
8        ans: list[int] = []
9        # Maintain a sorted list so we can efficiently access the k-th largest element.
10        sorted_nums = SortedList(nums)
11        mod = 10**9 + 7
12
13        for val, k in queries:
14            # Insert the new value while keeping the list sorted.
15            sorted_nums.add(val)
16            # sorted_nums[-k] is the k-th largest element.
17            # Note: p is reassigned each iteration, so the base accumulates over queries.
18            p = pow(p, sorted_nums[-k], mod)
19            ans.append(p)
20
21        return ans
22```
23
24A few notes on the changes and observations:
25
26- **Import added**: `SortedList` comes from the `sortedcontainers` library, which was missing in the original.
27- **Variable renaming**: `sl` β†’ `sorted_nums` for clarity.
28- **Type annotation**: Added `ans: list[int]` to make the return type explicit.
29- **Behavioral note**: The reassignment `p = pow(p, ...)` means the exponentiation base changes every iteration. If the intent was to keep the original `p` as a fixed base, this is a potential bugβ€”you'd want a separate variable like `result = pow(p, sorted_nums[-k], mod)`. I've preserved the original behavior since you asked for a rewrite, not a fix, but flagging it in case it matters.
30
31If you intended `p` to stay constant, here's the alternative:
32
33```python3
34from sortedcontainers import SortedList
35
36
37class Solution:
38    def powerUpdate(
39        self, nums: list[int], p: int, queries: list[list[int]]
40    ) -> list[int]:
41        ans: list[int] = []
42        sorted_nums = SortedList(nums)
43        mod = 10**9 + 7
44
45        for val, k in queries:
46            sorted_nums.add(val)
47            # Keep p as a fixed base; compute the result independently each time.
48            result = pow(p, sorted_nums[-k], mod)
49            ans.append(result)
50
51        return ans
52
1import java.util.*;
2
3class Solution {
4    private static final int MOD = 1_000_000_007;
5
6    /**
7     * An order-statistics multiset implemented over a TreeSet.
8     *
9     * Java's standard library lacks a built-in order-statistics tree (unlike the
10     * C++ pb_ds tree), so duplicates are allowed by pairing each value with a
11     * unique tie-breaker id. Keys {value, id} remain distinct while still
12     * sorting primarily by value, then by id.
13     *
14     * To support find-by-order in O(log n), we keep an auxiliary Fenwick (BIT)
15     * keyed by compressed values. The TreeSet alone cannot answer the k-th
16     * element query efficiently, so the BIT carries the rank information.
17     */
18    public int[] powerUpdate(int[] nums, int p, int[][] queries) {
19        int n = nums.length;
20        int q = queries.length;
21        int[] ans = new int[q];
22
23        // ----- Coordinate compression -----
24        // Collect every value that can ever appear (initial array + queries),
25        // so the Fenwick tree can be indexed by a small, dense range.
26        TreeSet<Integer> distinct = new TreeSet<>();
27        for (int x : nums) {
28            distinct.add(x);
29        }
30        for (int[] query : queries) {
31            distinct.add(query[0]);
32        }
33
34        // Map each distinct value to a 1-based compressed index.
35        Map<Integer, Integer> valueToIndex = new HashMap<>();
36        int idx = 1;
37        for (int value : distinct) {
38            valueToIndex.put(value, idx++);
39        }
40        int distinctCount = distinct.size();
41
42        // sortedValues[i] gives the original value for compressed index (i + 1).
43        int[] sortedValues = new int[distinctCount];
44        idx = 0;
45        for (int value : distinct) {
46            sortedValues[idx++] = value;
47        }
48
49        // ----- Fenwick tree (counts how many elements share each value) -----
50        // Acts as the order-statistics structure: prefix sums give ranks,
51        // and a binary-search descent gives the k-th element by order.
52        Fenwick fenwick = new Fenwick(distinctCount);
53
54        // Insert all initial numbers. The multiset semantics are handled by
55        // incrementing the count at the value's compressed index.
56        for (int x : nums) {
57            fenwick.update(valueToIndex.get(x), 1);
58        }
59
60        // ----- Answer each query -----
61        for (int i = 0; i < q; i++) {
62            int val = queries[i][0]; // value to insert before answering this query
63            int k = queries[i][1];   // we want the k-th largest value currently stored
64
65            // Insert the queried value (increment its count by one).
66            fenwick.update(valueToIndex.get(val), 1);
67
68            // The k-th largest element sits at ascending order index (size - k).
69            // findByOrder uses 0-based ordering, matching find_by_order.
70            int order = fenwick.size() - k;
71            int compressedIndex = fenwick.findByOrder(order);
72            int kthLargest = sortedValues[compressedIndex - 1];
73
74            // Each query is answered independently: compute p^(kthLargest) % MOD.
75            ans[i] = (int) modPow(p, kthLargest);
76        }
77
78        return ans;
79    }
80
81    /** Fast modular exponentiation: computes (base ^ exp) % MOD. */
82    private long modPow(long base, long exp) {
83        long result = 1;
84        base %= MOD;
85        while (exp > 0) {
86            if ((exp & 1) == 1) {
87                result = (result * base) % MOD;
88            }
89            base = (base * base) % MOD;
90            exp >>= 1;
91        }
92        return result;
93    }
94
95    /**
96     * A Fenwick (Binary Indexed) tree that supports point updates and the
97     * order-statistics queries needed here: total size and k-th element by
98     * order (0-based), both in O(log n).
99     */
100    private static class Fenwick {
101        private final int n;       // number of compressed value slots
102        private final int[] bit;   // 1-based Fenwick storage
103        private int total;         // running count of all inserted elements
104
105        Fenwick(int n) {
106            this.n = n;
107            this.bit = new int[n + 1];
108            this.total = 0;
109        }
110
111        /** Add delta to the count at compressed index i (1-based). */
112        void update(int i, int delta) {
113            total += delta;
114            for (; i <= n; i += i & (-i)) {
115                bit[i] += delta;
116            }
117        }
118
119        /** Current number of elements stored across all values. */
120        int size() {
121            return total;
122        }
123
124        /**
125         * Returns the 1-based compressed index of the element at the given
126         * 0-based order position. Equivalent to find_by_order: it locates the
127         * smallest index whose prefix count exceeds 'order'.
128         */
129        int findByOrder(int order) {
130            // 'order' is 0-based, so we search for the (order + 1)-th element.
131            int target = order + 1;
132            int pos = 0;
133            int remaining = target;
134
135            // Highest power of two not exceeding n.
136            int logN = Integer.highestOneBit(n);
137            for (int step = logN; step > 0; step >>= 1) {
138                int next = pos + step;
139                if (next <= n && bit[next] < remaining) {
140                    pos = next;
141                    remaining -= bit[next];
142                }
143            }
144            // pos now points to the last index with cumulative count < target,
145            // so the answer is the next index.
146            return pos + 1;
147        }
148    }
149}
150
1#include <ext/pb_ds/assoc_container.hpp>
2#include <ext/pb_ds/tree_policy.hpp>
3#include <vector>
4
5using namespace std;
6using namespace __gnu_pbds;
7
8// An order-statistics tree acting as an "ordered multiset".
9// Duplicates are allowed by pairing each value with a unique tie-breaker id,
10// so {value, id} keys remain distinct while still sorting primarily by value.
11template <typename T>
12using ordered_multiset = tree<pair<T, int>, null_type, less<pair<T, int>>,
13    rb_tree_tag, tree_order_statistics_node_update>;
14
15class Solution {
16public:
17    vector<int> powerUpdate(vector<int>& nums, int p, vector<vector<int>>& queries) {
18        vector<int> ans;
19        ordered_multiset<int> tree_set;          // ordered multiset of {value, unique_id}
20        const int mod = 1e9 + 7;
21
22        // Insert all initial numbers, using the array index as the unique id.
23        for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
24            tree_set.insert({nums[i], i});
25        }
26
27        // Ids for values inserted by queries continue after the initial indices.
28        int next_id = static_cast<int>(nums.size());
29
30        // Fast modular exponentiation: computes (base ^ exp) % mod.
31        auto mod_pow = [&](long long base, long long exp) -> long long {
32            long long result = 1;
33            base %= mod;
34            while (exp > 0) {
35                if (exp & 1) {
36                    result = (result * base) % mod;
37                }
38                base = (base * base) % mod;
39                exp >>= 1;
40            }
41            return result;
42        };
43
44        for (const auto& query : queries) {
45            int val = query[0];   // value to insert before answering this query
46            int k = query[1];     // we want the k-th largest value currently stored
47
48            // Insert the queried value with a fresh unique id.
49            tree_set.insert({val, next_id++});
50
51            // The k-th largest element sits at index (size - k) in ascending order.
52            auto it = tree_set.find_by_order(tree_set.size() - k);
53            int kth_largest = it->first;
54
55            // Each query is answered independently: compute p^(kth_largest) % mod.
56            // (The original code reassigned p, which carried state between queries;
57            //  using a local result avoids that side effect.)
58            int result = static_cast<int>(mod_pow(p, kth_largest));
59            ans.push_back(result);
60        }
61
62        return ans;
63    }
64};
65
1/**
2 * Solves the powerUpdate problem.
3 *
4 * Since TypeScript/JavaScript has no built-in order-statistics tree,
5 * we emulate the "ordered multiset" with a sorted array. Each value is
6 * paired with a unique id so duplicate values remain distinct keys while
7 * still sorting primarily by value (and secondarily by id).
8 *
9 * @param nums    - initial numbers
10 * @param p       - exponentiation base
11 * @param queries - each query is [value, k]: insert `value`, then report
12 *                  p^(k-th largest value currently stored) % mod
13 * @returns the answer for each query
14 */
15function powerUpdate(nums: number[], p: number, queries: number[][]): number[] {
16    const ans: number[] = [];
17    const mod = 1_000_000_007n;
18
19    // The ordered multiset is represented as a sorted array of [value, id] pairs.
20    // We keep it sorted ascending by (value, id) so that index lookups behave
21    // like find_by_order on an order-statistics tree.
22    const sortedSet: Array<[number, number]> = [];
23
24    // Comparator: sort primarily by value, then by the unique tie-breaker id.
25    const compare = (a: [number, number], b: [number, number]): number => {
26        if (a[0] !== b[0]) {
27            return a[0] - b[0];
28        }
29        return a[1] - b[1];
30    };
31
32    // Find the leftmost insertion index that keeps the array sorted (lower_bound).
33    const lowerBound = (key: [number, number]): number => {
34        let low = 0;
35        let high = sortedSet.length;
36        while (low < high) {
37            const mid = (low + high) >>> 1;
38            if (compare(sortedSet[mid], key) < 0) {
39                low = mid + 1;
40            } else {
41                high = mid;
42            }
43        }
44        return low;
45    };
46
47    // Insert a [value, id] pair while preserving sorted order.
48    const insert = (key: [number, number]): void => {
49        const pos = lowerBound(key);
50        sortedSet.splice(pos, 0, key);
51    };
52
53    // Insert all initial numbers, using the array index as the unique id.
54    for (let i = 0; i < nums.length; ++i) {
55        insert([nums[i], i]);
56    }
57
58    // Ids for values inserted by queries continue after the initial indices.
59    let nextId = nums.length;
60
61    // Fast modular exponentiation: computes (base ^ exp) % mod using BigInt
62    // to avoid overflow during multiplication.
63    const modPow = (base: bigint, exp: bigint): bigint => {
64        let result = 1n;
65        base %= mod;
66        while (exp > 0n) {
67            if (exp & 1n) {
68                result = (result * base) % mod;
69            }
70            base = (base * base) % mod;
71            exp >>= 1n;
72        }
73        return result;
74    };
75
76    for (const query of queries) {
77        const val = query[0]; // value to insert before answering this query
78        const k = query[1];   // we want the k-th largest value currently stored
79
80        // Insert the queried value with a fresh unique id.
81        insert([val, nextId++]);
82
83        // The k-th largest element sits at index (size - k) in ascending order.
84        const kthLargest = sortedSet[sortedSet.length - k][0];
85
86        // Each query is answered independently: compute p^(kthLargest) % mod.
87        const result = Number(modPow(BigInt(p), BigInt(kthLargest)));
88        ans.push(result);
89    }
90
91    return ans;
92}
93

Time and Space Complexity

  • Time Complexity: O((n + m) Γ— log(n + m)), where n and m are the lengths of nums and queries, respectively. Building the SortedList from nums costs O(n Γ— log n). Then, for each of the m queries, sl.add(val) inserts an element into the SortedList, which takes O(log(n + m)) since the list grows up to size n + m; the index access sl[-k] is O(log(n + m)) for a SortedList, and the pow computation runs in O(log(mod)) time, which is constant relative to the input. Therefore, the total time complexity is O(n Γ— log n + m Γ— log(n + m)), simplified to O((n + m) Γ— log(n + m)).

  • Space Complexity: O(n + m), where n and m are the lengths of nums and queries, respectively. The SortedList holds the initial n elements plus one element added per query, reaching a maximum size of n + m. The answer list ans stores m results. Hence, the overall space complexity is O(n + m).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Misinterpreting whether p accumulates or stays fixed

The most subtle and dangerous pitfall is misunderstanding how p evolves across queries. The problem statement says "Update p to p^x % (10^9 + 7)" and "Each query builds on the result of the previous one, since p is continuously updated throughout the process." This means p is reassigned each iteration, so the base of the next exponentiation is the result of the previous one.

A common mistake is to treat p as a fixed base and compute each answer independently:

# WRONG: p never changes, so each answer uses the original p
for val, k in queries:
    sorted_nums.add(val)
    result = pow(p, sorted_nums[-k], mod)  # p stays constant
    ans.append(result)

Correct approach β€” reassign p so it carries forward:

for val, k in queries:
    sorted_nums.add(val)
    p = pow(p, sorted_nums[-k], mod)  # p accumulates
    ans.append(p)

To see the difference, suppose p = 2 and the first two exponents are 3 and 2:

  • Accumulating: p becomes 2^3 = 8, then 8^2 = 64.
  • Fixed base: answers are 2^3 = 8, then 2^2 = 4.

Always re-read whether the state persists between queries before choosing.

Pitfall 2: Indexing the k-th largest element incorrectly

Since the list is sorted in ascending order, the k-th largest element lives at index -k, not k - 1 (that would give the k-th smallest). A frequent off-by-one error is writing sorted_nums[k] or sorted_nums[k - 1]:

# WRONG: this is the k-th SMALLEST
x = sorted_nums[k - 1]

# CORRECT: k-th LARGEST in an ascending list
x = sorted_nums[-k]

Verify with a quick example: in [1, 3, 5, 7], the 1st largest is 7 = sorted_nums[-1], and the 2nd largest is 5 = sorted_nums[-2].

Pitfall 3: Computing the power without modular exponentiation

The exponents x can be large, and chaining exponentiation makes intermediate values explode. Attempting (p ** x) % mod manually computes a gigantic integer first, which is extremely slow (and in many languages, overflows). Python's built-in three-argument pow performs fast modular exponentiation, applying the modulus throughout:

# SLOW / overflow-prone: builds the full power first
p = (p ** sorted_nums[-k]) % mod

# FAST: modular exponentiation in O(log x)
p = pow(p, sorted_nums[-k], mod)

Always use pow(base, exp, mod) for modular power operations.

Pitfall 4: Reaching for a plain list instead of a balanced structure

It is tempting to use an ordinary list and call sort() after each insertion, or bisect.insort. While bisect.insort finds the position in O(log n), the actual insertion still shifts elements in O(n), giving O(q Γ— n) overall. Re-sorting after every insert is even worse at O(q Γ— n log n). For large inputs this causes timeouts. A SortedList keeps both insertion and indexed access efficient (roughly O(log n) and O(log n) respectively), which is essential when n and q are large.

Pitfall 5: Forgetting that k is always valid relative to the current size

The k-th largest is taken after the insertion, so the array has grown by one before the lookup. If you compute sorted_nums[-k] before inserting val, you may index out of range when k equals the new size, or simply pick the wrong element. Always insert first, then query:

for val, k in queries:
    sorted_nums.add(val)        # insert FIRST
    p = pow(p, sorted_nums[-k], mod)  # then look up
    ans.append(p)

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More