LeetCode 3524 & 3525: From a static to a dynamic solution
The main difference between LeetCode 3524. Find X Value of Array I and LeetCode 3525. Find X Value of Array II is that the second problem introduces dynamic updates.
3524. Find X Value of Array I
In 3524 problem, the nums array does not change. We need to count how many ways we can remove a prefix and a suffix while keeping the array non-empty so that:
product % k == xSince the array is static, we can process it from left to right and maintain the number of products for each possible remainder:
0, 1, ..., k - 1This is essentially Dynamic Programming over remainders + Modular Arithmetic technique, so we do not need to use a Segment Tree pattern.
Hence, we can solve a LeetCode 3524 problem using this technique:
static nums, then process elements once, then Dynamic Programming over remainders, and then get answer.
Complexity is:
Time: O(n * k)
Space: O(k)
Since k <= 5, this is effectively:
Time complexity: O(n)
Space complexity: O(1)
3525. Find X Value of Array II
In 3525 problem, we have multiple queries:
[index, value, start, x]Each query first performs a permanent update:
nums[index] = valueThe updated value remains in the array for all following queries.
After the update, we need to calculate the x-value for:
nums[start ... n - 1]So now we have a dynamic sequence of operations:
nums - point update - query [start, n - 1] - point update - query [start, n - 1] - ...
If we simply ran the 3524 algorithm again after every update, the complexity would be approximately O(q * n * k) - which is too slow.
Instead, 3525 problem uses a Segment Tree pattern. Each node stores:
prod
cnt[0 ... k - 1]Here, prod is the product of the entire segment modulo k, while cnt[r] stores the number of non-empty prefixes whose product has remainder r.
This allows us to perform update nums[index] it O(k * log n) time and query [start, n - 1] in O(k * log n) time.
The total complexity is O(n * k + q * k * log n).
Since k <= 5 we have O(n + q * log n).
Time complexity: O(n + q * k * log n) = O(n + q * log n)
Space complexity: O(n * k) = O(n)
where q is the number of queries.
In short, LeetCode 3525 problem is the dynamic version of the idea used in LeetCode 3524 problem. The remainder-counting idea is still the same, but because the array changes after every query, we need a Segment Tree patern to update and recompute only the affected parts efficiently.
LeetCode 3524 C++ solution
class Solution {
public:
vector<long long> resultArray(vector<int>& nums, int k) {
vector<long long> result(k, 0);
vector<long long> dp(k, 0);
for(int num : nums)
{
vector<long long> next(k, 0);
int value = num % k;
// Start a new subarray.
next[value]++;
// Extend all subarrays ending at the previous position.
for(int r = 0; r < k; r++)
{
int newRemainder = (r * value) % k;
next[newRemainder] += dp[r];
}
for(int r = 0; r < k; r++) result[r] += next[r];
dp = move(next);
}
return result;
}
};LeetCode 3525 C++ solution
class Solution {
// Each node stores the product of the whole segment modulo k
// and the number of prefixes for every possible remainder.
struct Node {
int prod = 1;
array<int, 5> cnt{};
};
int n, k;
vector<Node> tree;
// Merge two adjacent segments.
// Keep all prefixes from the left segment.
// Prefixes from the right segment are multiplied by the full left product.
Node mergeNodes(const Node& left, const Node& right) {
Node res;
res.prod = (left.prod * right.prod) % k;
for(int r = 0; r < k; r++) res.cnt[r] += left.cnt[r];
for(int r = 0; r < k; r++) {
int nr = (left.prod * r) % k;
res.cnt[nr] += right.cnt[r];
}
return res;
}
// Build the segment tree.
// A leaf has exactly one non-empty prefix: the element itself.
void build(int node, int l, int r, const vector<int>& nums) {
if(l == r) {
int rem = nums[l] % k;
tree[node].prod = rem;
tree[node].cnt[rem] = 1;
return;
}
int mid = l + (r - l) / 2;
build(node * 2, l, mid, nums);
build(node * 2 + 1, mid + 1, r, nums);
tree[node] = mergeNodes(tree[node * 2], tree[node * 2 + 1]);
}
// Permanently update nums[index] and rebuild the affected tree nodes.
void update(int node, int l, int r, int index, int value) {
if(l == r) {
tree[node] = Node{};
int rem = value % k;
tree[node].prod = rem;
tree[node].cnt[rem] = 1;
return;
}
int mid = l + (r - l) / 2;
if(index <= mid) update(node * 2, l, mid, index, value);
else update(node * 2 + 1, mid + 1, r, index, value);
tree[node] = mergeNodes(tree[node * 2], tree[node * 2 + 1]);
}
// Return the combined information for range [ql, qr].
// For each query this range is [start, n - 1].
Node query(int node, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) return tree[node];
int mid = l + (r - l) / 2;
if(qr <= mid) return query(node * 2, l, mid, ql, qr);
if(ql > mid) return query(node * 2 + 1, mid + 1, r, ql, qr);
Node left = query(node * 2, l, mid, ql, qr);
Node right = query(node * 2 + 1, mid + 1, r, ql, qr);
return mergeNodes(left, right);
}
public:
vector<int> resultArray(vector<int>& nums, int k, vector<vector<int>>& queries) {
this->n = nums.size();
this->k = k;
// Build the tree for the initial array.
tree.resize(4 * n);
build(1, 0, n - 1, nums);
vector<int> result;
result.reserve(queries.size());
// Apply each permanent update, then analyze nums[start..n - 1].
for(const auto& q : queries) {
int index = q[0];
int value = q[1];
int start = q[2];
int x = q[3];
update(1, 0, n - 1, index, value);
// cnt[x] is the number of prefixes of nums[start..n - 1]
// whose product modulo k equals x.
Node res = query(1, 0, n - 1, start, n - 1);
result.push_back(res.cnt[x]);
}
return result;
}
};