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