Files
codetutor/backend/data/questions/build-array-where-you-can-find-the-maximum-exactly-k-comparisons.yaml

247 lines
11 KiB
YAML

title: Build Array Where You Can Find The Maximum Exactly K Comparisons
slug: build-array-where-you-can-find-the-maximum-exactly-k-comparisons
difficulty: hard
leetcode_id: 1420
leetcode_url: https://leetcode.com/problems/build-array-where-you-can-find-the-maximum-exactly-k-comparisons/
categories:
- arrays
- dynamic-programming
patterns:
- dynamic-programming
- prefix-sum
description: |
You are given three integers `n`, `m` and `k`. Consider the following algorithm to find the maximum element of an array of positive integers:
```
maximum = arr[0]
search_cost = 1
for i in range(1, len(arr)):
if arr[i] > maximum:
maximum = arr[i]
search_cost += 1
```
You should build the array `arr` which has the following properties:
- `arr` has exactly `n` integers.
- `1 <= arr[i] <= m` where `(0 <= i < n)`.
- After applying the mentioned algorithm to `arr`, the value `search_cost` is equal to `k`.
Return *the number of ways* to build the array `arr` under the mentioned conditions. As the answer may grow large, the answer **must be** computed modulo `10^9 + 7`.
constraints: |
- `1 <= n <= 50`
- `1 <= m <= 100`
- `0 <= k <= n`
examples:
- input: "n = 2, m = 3, k = 1"
output: "6"
explanation: "The possible arrays are [1, 1], [2, 1], [2, 2], [3, 1], [3, 2], [3, 3]"
- input: "n = 5, m = 2, k = 3"
output: "0"
explanation: "There are no possible arrays that satisfy the mentioned conditions."
- input: "n = 9, m = 1, k = 1"
output: "1"
explanation: "The only possible array is [1, 1, 1, 1, 1, 1, 1, 1, 1]"
explanation:
intuition: |
Imagine building an array element by element, keeping track of two things: the **current maximum** value we've placed, and how many times we've **increased** that maximum (the search cost).
The key insight is that each element we place falls into one of two categories:
1. **Non-increasing**: The element is less than or equal to the current maximum. This doesn't change the search cost.
2. **New maximum**: The element is strictly greater than the current maximum. This increases the search cost by 1.
Think of it like climbing stairs where each "step up" to a new maximum counts as a comparison. We need exactly `k` such steps across all `n` positions.
This naturally leads to a 3D dynamic programming approach where we track:
- How many positions we've filled (`i`)
- What the current maximum value is (`max_val`)
- How many times we've found a new maximum (`cost`)
For each state, we count how many ways we can reach it by considering all possible values for the next element.
approach: |
We use **3D Dynamic Programming** with prefix sum optimisation:
**Step 1: Define the DP state**
- `dp[i][max_val][cost]`: Number of ways to build an array of length `i` where:
- The current maximum element is `max_val`
- We've encountered exactly `cost` new maximums so far
&nbsp;
**Step 2: Establish base cases**
- For a single-element array (length 1), any value `v` from 1 to `m` gives us exactly 1 search cost
- `dp[1][v][1] = 1` for all `v` in `[1, m]`
&nbsp;
**Step 3: Define transitions**
For each position `i` from 2 to `n`:
- **Adding a non-increasing element**: If the current max is `max_val`, we can add any value from 1 to `max_val` without changing the cost. This contributes `max_val * dp[i-1][max_val][cost]` ways.
- **Adding a new maximum**: If we want the new max to be `new_max`, we can transition from any previous state where the max was less than `new_max`. This increases the cost by 1.
&nbsp;
**Step 4: Apply prefix sum optimisation**
The naive transition for adding a new maximum requires summing over all previous max values, giving O(m) per state. We can use prefix sums to compute these sums in O(1), reducing the overall complexity.
&nbsp;
**Step 5: Compute the answer**
- Sum `dp[n][max_val][k]` for all possible `max_val` from 1 to `m`
- Return the result modulo `10^9 + 7`
common_pitfalls:
- title: Forgetting the Modulo Operation
description: |
With constraints up to `n = 50` and `m = 100`, the number of valid arrays can be astronomically large. Forgetting to apply the modulo `10^9 + 7` at each step will cause integer overflow.
Always apply the modulo after every addition and multiplication in DP transitions.
wrong_approach: "Compute final answer, then apply modulo once"
correct_approach: "Apply modulo after each addition in transitions"
- title: O(n * m^2 * k) Time Limit Exceeded
description: |
A straightforward DP transition where we iterate over all previous max values for each new max results in O(m) per state transition. With states of size O(n * m * k) and O(m) transitions, this gives O(n * m^2 * k) complexity.
For `n = 50`, `m = 100`, `k = 50`, this is about 25 million operations per transition factor, which may be too slow.
Using prefix sums to precompute cumulative counts reduces transitions to O(1), bringing total complexity to O(n * m * k).
wrong_approach: "Nested loop over all previous max values"
correct_approach: "Prefix sum for O(1) transition lookups"
- title: Off-by-One in Search Cost
description: |
The search cost starts at 1 (the first element is always the initial maximum), not 0. Be careful when initialising base cases and when handling `k = 0`.
If `k = 0`, there are **no valid arrays** since placing even one element gives a search cost of at least 1.
wrong_approach: "Initialise cost from 0"
correct_approach: "Base case has cost = 1 for single-element arrays"
key_takeaways:
- "**3D DP for counting**: When counting arrangements with multiple constraints (length, max value, cost), use multi-dimensional DP where each dimension tracks one constraint"
- "**Prefix sum optimisation**: When DP transitions involve summing over a range of previous states, precompute prefix sums to reduce per-state transition cost from O(m) to O(1)"
- "**Modular arithmetic**: For counting problems with large answers, apply modulo at every step to prevent overflow"
- "**State definition is key**: Identifying the right state variables (position, current max, cost) makes the recurrence relation straightforward"
time_complexity: "O(n * m * k). We fill a 3D DP table of size `n * m * k`, and with prefix sum optimisation, each state transition takes O(1) time."
space_complexity: "O(n * m * k) for the DP table. Can be optimised to O(m * k) by only keeping two layers (current and previous position)."
solutions:
- approach_name: 3D Dynamic Programming with Prefix Sum
is_optimal: true
code: |
def num_of_arrays(n: int, m: int, k: int) -> int:
MOD = 10**9 + 7
# dp[i][max_val][cost] = number of ways to build array of length i
# with current max = max_val and search_cost = cost
# Using 1-indexed for max_val and cost for clarity
dp = [[[0] * (k + 2) for _ in range(m + 2)] for _ in range(n + 1)]
# Base case: arrays of length 1
# Any value v from 1 to m gives search_cost = 1
for v in range(1, m + 1):
dp[1][v][1] = 1
# Fill DP table
for i in range(2, n + 1):
# Prefix sum for transitioning to new maximum
# prefix[cost] = sum of dp[i-1][1..max_val-1][cost-1]
prefix = [0] * (k + 2)
for max_val in range(1, m + 1):
# Update prefix sums with previous max_val's contribution
for cost in range(1, k + 1):
prefix[cost] = (prefix[cost] + dp[i - 1][max_val - 1][cost - 1]) % MOD
for cost in range(1, k + 1):
# Case 1: Add element <= max_val (no cost increase)
# We can add any of 1..max_val, so multiply by max_val
ways = (dp[i - 1][max_val][cost] * max_val) % MOD
# Case 2: This position sets a new maximum
# We transition from states where old max < max_val
# and cost was (cost - 1)
ways = (ways + prefix[cost]) % MOD
dp[i][max_val][cost] = ways
# Sum all ways to build array of length n with search_cost = k
result = 0
for max_val in range(1, m + 1):
result = (result + dp[n][max_val][k]) % MOD
return result
explanation: |
**Time Complexity:** O(n * m * k) — We iterate through all states and use prefix sums for O(1) transitions.
**Space Complexity:** O(n * m * k) — Full 3D DP table storage.
The key optimisation is maintaining prefix sums as we iterate through `max_val`. When we're at `max_val`, the prefix sum already contains the cumulative count of all states with smaller max values, allowing us to compute the "new maximum" transition in O(1).
- approach_name: Space-Optimised DP
is_optimal: false
code: |
def num_of_arrays(n: int, m: int, k: int) -> int:
MOD = 10**9 + 7
# Only keep current and previous layers
# prev[max_val][cost] = ways for previous position
# curr[max_val][cost] = ways for current position
prev = [[0] * (k + 2) for _ in range(m + 2)]
curr = [[0] * (k + 2) for _ in range(m + 2)]
# Base case: length 1 arrays
for v in range(1, m + 1):
prev[v][1] = 1
# Build array position by position
for i in range(2, n + 1):
# Reset current layer
for max_val in range(m + 2):
for cost in range(k + 2):
curr[max_val][cost] = 0
# Prefix sum for new maximum transitions
prefix = [0] * (k + 2)
for max_val in range(1, m + 1):
# Update prefix with previous max_val
for cost in range(1, k + 1):
prefix[cost] = (prefix[cost] + prev[max_val - 1][cost - 1]) % MOD
for cost in range(1, k + 1):
# Non-increasing: multiply by max_val choices
ways = (prev[max_val][cost] * max_val) % MOD
# New maximum: use prefix sum
ways = (ways + prefix[cost]) % MOD
curr[max_val][cost] = ways
# Swap layers
prev, curr = curr, prev
# Sum final answers
result = 0
for max_val in range(1, m + 1):
result = (result + prev[max_val][k]) % MOD
return result
explanation: |
**Time Complexity:** O(n * m * k) — Same as the full DP solution.
**Space Complexity:** O(m * k) — We only store two layers instead of all `n` layers.
This optimisation works because each position only depends on the previous position. We alternate between two 2D arrays, reducing memory usage significantly for large `n`.