269 lines
11 KiB
YAML
269 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:
|
|
- slug: dynamic-programming
|
|
is_optimal: false
|
|
- slug: prefix-sum
|
|
is_optimal: true
|
|
|
|
function_signature: "def num_of_arrays(n: int, m: int, k: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { n: 2, m: 3, k: 1 }
|
|
expected: 6
|
|
- input: { n: 5, m: 2, k: 3 }
|
|
expected: 0
|
|
- input: { n: 9, m: 1, k: 1 }
|
|
expected: 1
|
|
hidden:
|
|
- input: { n: 1, m: 1, k: 1 }
|
|
expected: 1
|
|
- input: { n: 1, m: 5, k: 1 }
|
|
expected: 5
|
|
- input: { n: 3, m: 3, k: 2 }
|
|
expected: 18
|
|
- input: { n: 2, m: 2, k: 2 }
|
|
expected: 1
|
|
|
|
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
|
|
|
|
|
|
|
|
**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]`
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
**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`.
|