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 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`.