Skip to content
← All posts

Number of Squareful Arrays: Backtracking with Graph Modeling

6 min read
leetcodeproblemhardarraysbacktrackingmath

LeetCode Number of Squareful Arrays (problem 996) asks you to count the number of permutations of an integer array where every pair of adjacent elements sums to a perfect square. A permutation is called "squareful" if this property holds for all consecutive pairs. Given the array nums, return the number of squareful permutations.

sum=9sum=25sum=18 (not square)1178squareful pairnot squareful
Adjacency graph for [1, 17, 8]. An edge connects two numbers whose sum is a perfect square. Valid permutations are Hamiltonian paths through this graph.

Why this problem matters

This problem sits at the intersection of three core algorithmic ideas: backtracking, graph theory, and number theory. You need to recognize that the adjacency constraint (sum must be a perfect square) transforms the problem from raw permutation enumeration into a graph traversal problem. Once you see that connection, the solution becomes much cleaner. It is a great example of how reframing a combinatorial problem as a graph problem can simplify both the reasoning and the implementation.

The key insight

Think of each element in the array as a node in a graph. Draw an edge between two nodes if their sum is a perfect square. Now the question becomes: how many Hamiltonian paths exist in this graph? A Hamiltonian path visits every node exactly once, which is exactly what a permutation does.

For example, with [1, 17, 8], the graph has edges between 1 and 8 (since 1+8=9=3x3) and between 8 and 17 (since 8+17=25=5x5). There is no edge between 1 and 17 because 1+17=18 is not a perfect square. The Hamiltonian paths are 1 -> 8 -> 17 and 17 -> 8 -> 1, giving an answer of 2.

The backtracking approach tries each unused node as the next step, but only follows edges that exist in the adjacency graph. This prunes the search space dramatically compared to generating all permutations and checking each one.

The solution

from math import isqrt
from collections import Counter

def num_squareful_perms(nums):
    n = len(nums)
    count = Counter(nums)

    def is_square(x):
        s = isqrt(x)
        return s * s == x

    graph = {}
    unique = list(count.keys())
    for i in range(len(unique)):
        graph[unique[i]] = []
        for j in range(len(unique)):
            if is_square(unique[i] + unique[j]):
                graph[unique[i]].append(unique[j])

    result = 0

    def backtrack(val, remaining):
        nonlocal result
        if remaining == 0:
            result += 1
            return
        for neighbor in graph[val]:
            if count[neighbor] > 0:
                count[neighbor] -= 1
                backtrack(neighbor, remaining - 1)
                count[neighbor] += 1

    for val in count:
        count[val] -= 1
        backtrack(val, n - 1)
        count[val] += 1

    return result

The solution builds an adjacency graph where each unique value maps to a list of values it can be adjacent to (their sum is a perfect square). Instead of tracking indices, we use a Counter to handle duplicates naturally. When we decrement and increment the count for each value during backtracking, duplicate values are automatically treated as interchangeable, which prevents counting the same permutation more than once.

The outer loop tries each unique value as the starting element. For each start, the backtracking function explores all valid neighbors that still have remaining count, building the permutation one step at a time.

Using a Counter instead of a visited array is the key to handling duplicates cleanly. If the array has repeated values like [2, 2, 2], the Counter ensures you only count distinct permutations. You never need to sort and skip like in traditional duplicate-handling backtracking, because the Counter naturally groups identical elements together.

Visual walkthrough

Step 1: Check all pairs for perfect square sums

Pair sums:1 + 8 = 9 = 3x3valid8 + 17 = 25 = 5x5valid1 + 17 = 18not a perfect square

For [1, 17, 8], compute pairwise sums: 1+17=18, 1+8=9, 17+8=25. Only 9 and 25 are perfect squares, so 1 can be adjacent to 8, and 8 can be adjacent to 17.

Step 2: Model as a graph and find Hamiltonian paths

9251817

Each number is a node. Edges connect pairs with a perfect square sum. A valid squareful permutation is a path that visits every node exactly once (a Hamiltonian path).

Step 3: Backtrack from node 1

path = [1]neighbors of 1: {8} (1+8=9)path = [1, 8]neighbors of 8: {17} (8+17=25, skip 1 used)path = [1, 8, 17]All nodes visited. Valid permutation found.

Start with 1. Neighbors of 1 with square sum: only 8. From 8, neighbors with square sum: 1 (used) and 17 (available). Pick 17. All nodes visited. Valid path found: [1, 8, 17].

Step 4: Backtrack from node 17

path = [17]neighbors of 17: {8} (17+8=25)path = [17, 8]neighbors of 8: {1} (8+1=9, skip 17 used)path = [17, 8, 1]All nodes visited. Valid permutation found.

Start with 17. Neighbors with square sum: only 8. From 8, neighbors: 1 (available) and 17 (used). Pick 1. All visited. Valid path: [17, 8, 1].

Step 5: Backtrack from node 8 (dead end)

path = [8]neighbors of 8: {1, 17}try [8, 1]1 has no unvisited squareful neighbor. Dead end.try [8, 17]17 has no unvisited squareful neighbor. Dead end.No valid permutation starts with 8.

Start with 8. Neighbors: 1 (1+8=9) and 17 (8+17=25). Try 1 first: path [8, 1]. From 1, neighbor 8 is used, and 1+17=18 is not a perfect square. Dead end. Try 17: path [8, 17]. From 17, neighbor 8 is used, and 17+1=18 is not square. Dead end.

Step 6: Collect results

18171+8=9, 8+17=25178117+8=25, 8+1=9

Two valid squareful permutations found. The answer is 2.

The walkthrough above uses the example [1, 17, 8]. Notice how starting from node 8 leads to dead ends in both directions, because 8 sits in the middle of the only valid chain. The graph structure makes it clear why only two permutations work: you can traverse the chain left-to-right or right-to-left, but you cannot start from the middle and reach both endpoints.

Complexity analysis

ApproachTimeSpace
BacktrackingO(n! * n)O(n)

The worst-case time complexity is O(n!) because in the degenerate case every pair of elements could have a perfect square sum, meaning every permutation is valid. The extra factor of n comes from the path length at each recursive call. In practice, the adjacency constraint prunes most branches, so the actual running time is much better than n factorial for typical inputs.

The space complexity is O(n) for the recursion stack and the Counter. The adjacency graph uses O(n^2) space in the worst case for storing edges between all unique pairs, but since n is at most 12 in this problem, the space is negligible.

The building blocks

1. Perfect square check

The helper function is_square(x) uses integer square root to avoid floating-point precision issues. By computing isqrt(x) and squaring the result, you get an exact check. This is a common technique in problems involving perfect squares, and you will see it in many number theory problems. Never use sqrt from the math module and compare with a float, because rounding errors can produce wrong answers for large numbers.

2. Backtracking with duplicate handling

The standard backtracking template picks an element, recurses, then undoes the pick. The twist here is using a Counter to track available elements by value rather than by index. This approach collapses all identical values into a single count, so the backtracking tree never branches into two identical subtrees. If you have three 2s in the array, the counter says "I have three 2s available" rather than distinguishing between the first, second, and third 2.

Edge cases

  • All elements identical. If every element is the same value v, then v + v must be a perfect square for any valid permutation. If it is, there is exactly one distinct permutation (they are all the same). If not, the answer is 0.
  • Array of length 1. A single element is trivially squareful. The answer is 1.
  • No valid pairs. If no two elements sum to a perfect square, the answer is 0 for arrays of length 2 or more.
  • Large duplicate counts. With n up to 12 and many duplicates, the Counter-based approach prevents exponential blowup from treating identical elements as distinct.
  • Two elements. Check if their sum is a perfect square. If the two values are different, the answer is 2 (two orderings) when the sum is square, 0 otherwise. If they are the same, the answer is 1 when 2 * v is a perfect square.

From understanding to recall

The core insight to internalize is the graph modeling step. When you see a constraint on adjacent pairs in a permutation, think "graph where edges represent valid adjacencies" and then think "count Hamiltonian paths." That mental translation is the hardest part to recall under pressure.

Practice reconstructing the solution in three phases. First, write the is_square helper. Second, build the adjacency graph from unique values. Third, write the backtracking loop with the Counter. Each phase is small and self-contained, which makes the whole solution easier to hold in memory.

The duplicate handling trick (Counter instead of visited set) is worth practicing separately. Try applying it to Permutations II or similar problems where you need distinct permutations of an array with repeated elements. Once the pattern feels natural, you will not need to think about it during an interview.

Related posts

  • Permutations II - Generating all distinct permutations of an array with duplicates using sorting and backtracking
  • Combination Sum II - Backtracking with pruning to avoid duplicate combinations from a candidate list
  • Number of Islands - Graph traversal patterns for exploring connected components in a grid

The Number of Squareful Arrays problem is a beautiful blend of graph modeling and constrained backtracking. Once you see the adjacency graph, the solution writes itself. The Counter-based duplicate handling is the final piece that makes it clean and efficient. Practice the three-phase reconstruction until you can write it from scratch without hesitation.