title: Beautiful Arrangement
slug: beautiful-arrangement
difficulty: medium
leetcode_id: 526
leetcode_url: https://leetcode.com/problems/beautiful-arrangement/
categories:
  - arrays
  - recursion
patterns:
  - backtracking

function_signature: "def count_arrangement(n: int) -> int:"

test_cases:
  visible:
    - input: { n: 2 }
      expected: 2
    - input: { n: 1 }
      expected: 1
  hidden:
    - input: { n: 3 }
      expected: 3
    - input: { n: 4 }
      expected: 8
    - input: { n: 5 }
      expected: 10
    - input: { n: 6 }
      expected: 36
    - input: { n: 10 }
      expected: 700

description: |
  Suppose you have `n` integers labelled `1` through `n`. A permutation of those `n` integers `perm` (**1-indexed**) is considered a **beautiful arrangement** if for every `i` (`1 <= i <= n`), **either** of the following is true:

  - `perm[i]` is divisible by `i`.
  - `i` is divisible by `perm[i]`.

  Given an integer `n`, return *the **number** of the **beautiful arrangements** that you can construct*.

constraints: |
  - `1 <= n <= 15`

examples:
  - input: "n = 2"
    output: "2"
    explanation: |
      The first beautiful arrangement is [1, 2]:
      - perm[1] = 1 is divisible by i = 1
      - perm[2] = 2 is divisible by i = 2

      The second beautiful arrangement is [2, 1]:
      - perm[1] = 2 is divisible by i = 1
      - i = 2 is divisible by perm[2] = 1
  - input: "n = 1"
    output: "1"
    explanation: "The only arrangement is [1], which satisfies perm[1] = 1 divisible by i = 1."

explanation:
  intuition: |
    Imagine you're filling `n` numbered boxes (positions 1 through n) with `n` numbered balls (values 1 through n). Each ball can only go in a box if there's a "divisibility relationship" between them — either the ball number divides the box number, or vice versa.

    This is fundamentally a **constraint satisfaction problem**. You need to count all valid ways to assign balls to boxes where every assignment satisfies the divisibility rule.

    Think of it like this: start filling boxes from position 1. For each position, try placing each unused number and check if it satisfies the divisibility condition. If it does, recursively try to fill the remaining positions. If you successfully fill all positions, you've found one beautiful arrangement. If you hit a dead end (no valid number for a position), backtrack and try a different number at the previous position.

    The key insight is that `n <= 15` is small enough that exploring the solution space via backtracking is feasible — we won't hit time limits even though there are `n!` potential permutations, because we **prune invalid branches early**.

  approach: |
    We solve this using **Backtracking** to explore all valid permutations:

    **Step 1: Define the recursive function**

    - Create a helper function `backtrack(position, used)` that tries to fill `position` using numbers from the `used` set
    - `position`: The current position we're trying to fill (1-indexed)
    - `used`: A set tracking which numbers have already been placed

    &nbsp;

    **Step 2: Base case**

    - If `position > n`, we've successfully filled all positions — increment our count and return
    - This means we found one valid beautiful arrangement

    &nbsp;

    **Step 3: Try each unused number**

    - For each number from `1` to `n`:
      - Skip if already used
      - Check the divisibility condition: `num % position == 0` or `position % num == 0`
      - If valid, mark the number as used and recurse to the next position
      - After returning, unmark the number (backtrack) to try other possibilities

    &nbsp;

    **Step 4: Return the count**

    - After exploring all branches, return the total count of valid arrangements

    &nbsp;

    The backtracking naturally prunes invalid branches — if no number works for a position, that branch terminates without exploring further, making this much faster than generating all `n!` permutations.

  common_pitfalls:
    - title: Generating All Permutations First
      description: |
        A naive approach might generate all `n!` permutations and then filter for valid ones. With `n = 15`, that's over 1.3 trillion permutations — clearly infeasible.

        The backtracking approach is far superior because it **prunes invalid branches early**. If position 1 can only accept certain numbers, we never explore branches where position 1 has an invalid number.
      wrong_approach: "Generate all permutations, then filter"
      correct_approach: "Backtrack with early pruning on divisibility check"

    - title: Off-by-One with 1-Indexed Positions
      description: |
        The problem uses **1-indexed** positions, meaning positions range from `1` to `n`, not `0` to `n-1`.

        If you start your recursion at position `0` and use `position % num` or `num % position`, you'll get division by zero or incorrect results. Always start at position `1`.
      wrong_approach: "Start recursion at position 0"
      correct_approach: "Start recursion at position 1 (1-indexed)"

    - title: Forgetting to Backtrack
      description: |
        After recursively exploring a branch with a number placed, you must **remove it from the used set** before trying the next number. Forgetting this means numbers stay "used" across different branches, leading to incorrect (lower) counts.

        Always pair "add to used" with "remove from used" after the recursive call returns.
      wrong_approach: "Add to used set but never remove"
      correct_approach: "Remove from used set after recursive call (backtrack)"

  key_takeaways:
    - "**Backtracking pattern**: When counting or finding all valid configurations, systematically explore choices and undo them (backtrack) to try alternatives"
    - "**Early pruning**: Check constraints as early as possible to avoid exploring invalid branches — this is what makes backtracking efficient"
    - "**Small constraints hint at exponential solutions**: When `n <= 15-20`, backtracking or bitmask DP is often the intended approach"
    - "**Foundation for harder problems**: This technique extends to N-Queens, Sudoku solvers, and other constraint satisfaction problems"

  time_complexity: "O(k) where k is the number of valid permutations. In the worst case, this approaches O(n!) but heavy pruning makes it much faster in practice."
  space_complexity: "O(n). The recursion stack depth is at most `n`, and we use a set of size `n` to track used numbers."

solutions:
  - approach_name: Backtracking
    is_optimal: true
    code: |
      def count_arrangement(n: int) -> int:
          count = 0

          def backtrack(position: int, used: set[int]) -> None:
              nonlocal count

              # Base case: filled all positions successfully
              if position > n:
                  count += 1
                  return

              # Try each number from 1 to n
              for num in range(1, n + 1):
                  # Skip if already used
                  if num in used:
                      continue

                  # Check divisibility condition
                  if num % position == 0 or position % num == 0:
                      # Place this number and recurse
                      used.add(num)
                      backtrack(position + 1, used)
                      # Backtrack: remove the number to try others
                      used.remove(num)

          backtrack(1, set())
          return count
    explanation: |
      **Time Complexity:** O(k) where k is the number of valid arrangements — significantly less than O(n!) due to pruning.

      **Space Complexity:** O(n) — recursion depth and the used set.

      We explore positions from 1 to n, trying each unused number that satisfies the divisibility condition. Invalid branches are pruned immediately, and we backtrack after each recursive call to explore all possibilities.

  - approach_name: Backtracking with Visited Array
    is_optimal: true
    code: |
      def count_arrangement(n: int) -> int:
          # Track which numbers are used (index 0 unused for 1-indexing)
          visited = [False] * (n + 1)
          count = 0

          def backtrack(position: int) -> None:
              nonlocal count

              # Successfully filled all positions
              if position > n:
                  count += 1
                  return

              for num in range(1, n + 1):
                  # Skip used numbers or those failing divisibility check
                  if visited[num]:
                      continue
                  if num % position != 0 and position % num != 0:
                      continue

                  # Choose this number for current position
                  visited[num] = True
                  backtrack(position + 1)
                  # Undo choice (backtrack)
                  visited[num] = False

          backtrack(1)
          return count
    explanation: |
      **Time Complexity:** O(k) where k is the number of valid arrangements.

      **Space Complexity:** O(n) — visited array and recursion stack.

      This variant uses a boolean array instead of a set for tracking used numbers. The logic is identical: try each valid number, recurse, then backtrack. Array lookups are O(1) like set operations, so performance is similar.

  - approach_name: Bitmask Dynamic Programming
    is_optimal: true
    code: |
      def count_arrangement(n: int) -> int:
          # dp[mask] = number of ways to arrange numbers in mask
          # where mask has k bits set and we've filled positions 1..k
          dp = [0] * (1 << n)
          dp[0] = 1  # Empty arrangement: one way to arrange nothing

          for mask in range(1 << n):
              # Count bits set = number of positions filled
              position = bin(mask).count('1') + 1

              # If we've filled more than n positions, skip
              if position > n + 1:
                  continue

              # Try adding each number not yet in mask
              for num in range(1, n + 1):
                  bit = 1 << (num - 1)

                  # Skip if number already used
                  if mask & bit:
                      continue

                  # Check divisibility condition
                  if num % position == 0 or position % num == 0:
                      dp[mask | bit] += dp[mask]

          # Full mask (all numbers used) gives our answer
          return dp[(1 << n) - 1]
    explanation: |
      **Time Complexity:** O(n * 2^n) — iterate through all 2^n masks, trying n numbers each.

      **Space Complexity:** O(2^n) — the DP table.

      This approach uses bitmask DP where `dp[mask]` represents the number of ways to fill positions using exactly the numbers represented by `mask`. We iterate through all subsets and transition by adding one number at a time. With `n <= 15`, 2^15 = 32,768 states is manageable.
