Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

996. Number of Squareful Arrays - Leetcode Solution

Code Implementation

from math import isqrt
from collections import Counter

def numSquarefulPerms(A):
    def is_square(num):
        root = isqrt(num)
        return root * root == num

    def dfs(x, count, length):
        if length == len(A):
            return 1
        total = 0
        for y in graph[x]:
            if count[y]:
                count[y] -= 1
                total += dfs(y, count, length + 1)
                count[y] += 1
        return total

    A.sort()
    graph = {x: [] for x in A}
    for i, x in enumerate(A):
        for j, y in enumerate(A):
            if i != j and is_square(x + y):
                graph[x].append(y)
    count = Counter(A)
    res = 0
    for x in count:
        count[x] -= 1
        res += dfs(x, count, 1)
        count[x] += 1
    return res
      
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#include <cmath>
using namespace std;

class Solution {
public:
    int numSquarefulPerms(vector<int>& A) {
        unordered_map<int, vector<int>> graph;
        int n = A.size();
        sort(A.begin(), A.end());
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i != j && isSquare(A[i] + A[j])) {
                    graph[A[i]].push_back(A[j]);
                }
            }
        }
        unordered_map<int, int> count;
        for (int x : A) count[x]++;
        int res = 0;
        for (auto& p : count) {
            count[p.first]--;
            res += dfs(p.first, count, graph, 1, n);
            count[p.first]++;
        }
        return res;
    }
private:
    bool isSquare(int num) {
        int root = sqrt(num);
        return root * root == num;
    }
    int dfs(int x, unordered_map<int, int>& count, unordered_map<int, vector<int>>& graph, int length, int n) {
        if (length == n) return 1;
        int total = 0;
        for (int y : graph[x]) {
            if (count[y]) {
                count[y]--;
                total += dfs(y, count, graph, length + 1, n);
                count[y]++;
            }
        }
        return total;
    }
};
      
import java.util.*;

class Solution {
    public int numSquarefulPerms(int[] A) {
        Map<Integer, List<Integer>> graph = new HashMap<>();
        Arrays.sort(A);
        int n = A.length;
        for (int x : A) graph.put(x, new ArrayList<>());
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i != j && isSquare(A[i] + A[j])) {
                    graph.get(A[i]).add(A[j]);
                }
            }
        }
        Map<Integer, Integer> count = new HashMap<>();
        for (int x : A) count.put(x, count.getOrDefault(x, 0) + 1);
        int res = 0;
        for (int x : count.keySet()) {
            count.put(x, count.get(x) - 1);
            res += dfs(x, count, graph, 1, n);
            count.put(x, count.get(x) + 1);
        }
        return res;
    }

    private boolean isSquare(int num) {
        int root = (int)Math.sqrt(num);
        return root * root == num;
    }

    private int dfs(int x, Map<Integer, Integer> count, Map<Integer, List<Integer>> graph, int length, int n) {
        if (length == n) return 1;
        int total = 0;
        for (int y : graph.get(x)) {
            if (count.get(y) != null && count.get(y) > 0) {
                count.put(y, count.get(y) - 1);
                total += dfs(y, count, graph, length + 1, n);
                count.put(y, count.get(y) + 1);
            }
        }
        return total;
    }
}
      
function numSquarefulPerms(A) {
    function isSquare(num) {
        let root = Math.sqrt(num);
        return root === Math.floor(root);
    }

    const n = A.length;
    A.sort((a, b) => a - b);
    const graph = {};
    for (let x of A) graph[x] = [];
    for (let i = 0; i < n; ++i) {
        for (let j = 0; j < n; ++j) {
            if (i !== j && isSquare(A[i] + A[j])) {
                graph[A[i]].push(A[j]);
            }
        }
    }
    const count = {};
    for (let x of A) count[x] = (count[x] || 0) + 1;

    function dfs(x, count, length) {
        if (length === n) return 1;
        let total = 0;
        for (let y of graph[x]) {
            if (count[y]) {
                count[y]--;
                total += dfs(y, count, length + 1);
                count[y]++;
            }
        }
        return total;
    }

    let res = 0;
    for (let x in count) {
        count[x]--;
        res += dfs(Number(x), count, 1);
        count[x]++;
    }
    return res;
}
      

Problem Description

Given an array A of integers, your task is to find out how many permutations of A form a "squareful array." An array is squareful if, for every pair of consecutive elements A[i] and A[i+1], the sum A[i] + A[i+1] is a perfect square (that is, the square of some integer). Each permutation must use every element of A exactly once, and duplicate permutations due to duplicate elements should be counted only once.

Thought Process

At first glance, it seems we could generate all possible permutations of A and count those where all adjacent sums are perfect squares. However, this brute-force approach is inefficient, especially with duplicate elements, since it explores many redundant cases. To optimize, we need to avoid duplicate permutations and prune impossible paths early. By thinking of the problem as building a path where each step connects to the next number if their sum is a perfect square, we can model the problem as a graph traversal. This leads us to consider depth-first search (DFS) with backtracking, using a counter to avoid reusing elements and to prevent counting duplicate permutations.

Solution Approach

The efficient solution involves the following steps:
  • 1. Build a Graph: For every pair of elements in A, check if their sum is a perfect square. If so, create an edge between them. This graph tells us which numbers can follow each other in a valid permutation.
  • 2. Count Occurrences: Use a hash map or counter to keep track of how many times each number appears in A. This helps us avoid reusing elements more than their actual occurrence.
  • 3. DFS with Backtracking: For each unique starting number, begin a DFS. At each step, try to extend the current path by adding a neighbor (from the graph) if it is still available (count > 0). Decrease the count when using a number and restore it when backtracking.
  • 4. Avoid Duplicates: By always traversing from unique numbers and using the count, we avoid counting duplicate permutations due to repeated elements.
  • 5. Aggregate Results: Sum up all valid paths (of length equal to A's length) found from all possible starting numbers.
This approach ensures we only consider valid and unique permutations, efficiently pruning impossible or duplicate paths.

Example Walkthrough

Suppose A = [1, 17, 8].
  • First, check all pairs:
    • 1 + 17 = 18 (not a perfect square)
    • 1 + 8 = 9 (3*3, yes!)
    • 17 + 1 = 18 (not a perfect square)
    • 17 + 8 = 25 (5*5, yes!)
    • 8 + 1 = 9 (3*3, yes!)
    • 8 + 17 = 25 (5*5, yes!)
  • Graph edges:
    • 1 is connected to 8
    • 8 is connected to 1 and 17
    • 17 is connected to 8
  • Start DFS from each unique number:
    • Start with 1: 1 → 8 (since 1+8=9), then 8 → 17 (8+17=25) → done. [1,8,17]
    • Start with 17: 17 → 8, then 8 → 1 → done. [17,8,1]
    • Start with 8: 8 → 1 (8+1=9), then 1 → 17 (1+17=18, not valid). Or 8 → 17 (8+17=25), then 17 → 1 (17+1=18, not valid). So only the above two valid permutations exist.
  • The answer is 2.
At each step, the DFS only proceeds along valid edges, and elements are not reused.

Time and Space Complexity

  • Brute-force approach: Generating all permutations of n elements is O(n!), and for each, checking adjacent pairs is O(n), leading to O(n! * n) time. Space is O(n) for recursion stack.
  • Optimized approach:
    • Building the graph: O(n^2) for all pairs.
    • DFS traversal: In the worst case (all elements unique and all edges valid), still O(n!) paths, but pruning and duplicate avoidance make it much faster in practice, especially with repeated elements.
    • Space: O(n^2) for the graph, O(n) for recursion stack, and O(n) for the count map.

Summary

The key to solving the Number of Squareful Arrays problem efficiently is to model the valid transitions as a graph and use DFS with backtracking, carefully managing element usage to avoid duplicates. By pruning invalid paths early and using a counter to handle repeated elements, the solution remains efficient and elegant, even for moderately sized arrays. This approach demonstrates how graph modeling and classic search techniques can turn a seemingly brute-force problem into a tractable one.