↓快轉到主要內容
  1. LeetCode/

LeetCode 996: Number of Squareful Arrays

·2 分鐘· ·
LeetCode Hard Dynamic-Programming Bitmask Backtracking Math Hash-Table
Wei Yi Chung
作者
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
目錄

基本資料
#

難易度: hard 第一次嘗試:2026-09-09

  • 提交紀錄:Runtime Error → Accepted

解題思路
#

n <= 12,可以用 bitmask 記錄哪些數字用過了。dp[mask][last] = 用了 mask 這些位置、最後一個放的是 nums[last] 的合法排列數:

  • 下一個數 nxt 還沒用過,而且 nums[last] + nums[nxt] 是平方數,就轉移到 dp[mask | 1 << nxt][nxt]。
  • 答案是 sum(dp[全部][last])。

去重:相同的數字交換位置算同一個排列。先排序,規定相同的數字要依 index 順序使用:nums[nxt] == nums[nxt - 1] 時,nxt - 1 必須已經用過,nxt 才能用。這樣每個排列只會出現一次。

我的嘗試
#

我寫的是回溯 + 剪枝(排序後跳過「前一個相同的數字還沒用」的情況),想法跟上面的去重一樣。第一版 RE:條件裡把 == 寫成 =,是語法錯誤;同一版還有 visited[num]、res += 1 沒有宣告 nonlocal、遞迴傳 i + 1 而不是 length + 1 這些問題,第二版一起修好就 AC 了(2.4 秒)。

正確解法
#

from math import isqrt

class Solution:
    def numSquarefulPerms(self, nums: List[int]) -> int:
        nums.sort()
        n = len(nums)
        is_sq = lambda x: isqrt(x) ** 2 == x
        ok = [[is_sq(nums[i] + nums[j]) for j in range(n)] for i in range(n)]

        dp = [[0] * n for _ in range(1 << n)]      # dp[mask][last]
        for i in range(n):
            if i == 0 or nums[i] != nums[i - 1]:   # 相同的數只從第一個開始
                dp[1 << i][i] = 1
        for mask in range(1 << n):
            for last in range(n):
                if not dp[mask][last]:
                    continue
                for nxt in range(n):
                    if mask >> nxt & 1 or not ok[last][nxt]:
                        continue
                    if nxt > 0 and nums[nxt] == nums[nxt - 1] and not mask >> (nxt - 1) & 1:
                        continue                   # 相同的數要依 index 順序用
                    dp[mask | 1 << nxt][nxt] += dp[mask][last]
        return sum(dp[(1 << n) - 1])

回溯版本:

from math import isqrt

class Solution:
    def numSquarefulPerms(self, nums: List[int]) -> int:
        nums.sort()
        n = len(nums)
        used = [False] * n

        def backtrack(length: int, prev: int) -> int:
            if length == n:
                return 1
            count = 0
            for i in range(n):
                if used[i]:
                    continue
                if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue                       # 去重
                if length == 0 or isqrt(prev + nums[i]) ** 2 == prev + nums[i]:
                    used[i] = True
                    count += backtrack(length + 1, nums[i])
                    used[i] = False
            return count

        return backtrack(0, -1)

我 AC 的寫法是 visited[i - 1](前一個相同的已經用過就跳過),這裡用 not used[i - 1]。兩種都能去重,只是固定的使用順序相反。

複雜度
#

bitmask DP:時間 O(2^n · n^2),空間 O(2^n · n)。回溯最壞也是指數級,但平方數的限制會剪掉很多。

要特別避免的錯誤
#

  • 沒有去重:相同的數字交換會被算成不同的排列。
  • 判斷平方數用 math.isqrt,不要用浮點數開根號。

面試口說整理
#

n 最多 12,我用 bitmask DP,dp[mask][last] 是用了 mask 這些數、最後一個是 last 的排列數,下一個數跟 last 加起來是平方數就能接。重複的數字規定依 index 順序使用,每個排列只算一次。時間 O(2^n · n^2)。也可以用回溯加同樣的去重。

相關文章

LeetCode 2376: Count Special Integers
·1 分鐘
LeetCode Hard Dynamic-Programming Digit-Dp Math Bitmask
LeetCode 2376 解題筆記:數位 DP,狀態 (pos, tight, started, mask);mask 記錄用過的數字,前導零不能算成用過 0
LeetCode 2912: Number of Ways to Reach Destination in the Grid
·2 分鐘
LeetCode Hard Dynamic-Programming State-Machine Math Combinatorics
LeetCode 2912 解題筆記:格子太多,但相對終點只分四類:終點、同一列、同一行、都不同;每一類的總方法數一起轉移,O(k)
LeetCode 952: Largest Component Size by Common Factor
·2 分鐘
LeetCode Hard Math Number-Theory Union-Find Hash-Table Array
LeetCode 952 解題筆記:有共同因數 ⇔ 有共同的質因數;把每個數分解質因數,同一個質因數的 index 用 Union-Find 合併,不用兩兩算 GCD
LeetCode 3518: Smallest Palindromic Rearrangement II
·2 分鐘
LeetCode Hard Math Combinatorics Counting Hash-Table String
LeetCode 3518 解題筆記:回文由左半邊決定,變成「多重集合的第 k 個排列」;逐位試每個字元,用排列數判斷第 k 個落在哪個分支,排列數可以 O(1) 更新
LeetCode 264: Ugly Number II
·1 分鐘
LeetCode Medium Hash-Table Math Dynamic-Programming Heap-Priority-Queue
LeetCode 264 解題筆記:三個指針分別指向下一個要乘 2、3、5 的醜數,每次取最小的,相等的指針都要前進
LeetCode 2963: Count the Number of Good Partitions
·1 分鐘
LeetCode Hard Math Combinatorics Hash-Table Array
LeetCode 2963 解題筆記:同一個數的所有出現位置必須在同一段,用最後出現位置把陣列切成 b 個不可再分的區塊;區塊之間可切可不切,答案是 2^(b-1)