- 标签:位运算、数组、动态规划、回溯、状态压缩
- 难度:中等
描述:给定一个整数
要求:返回可以构造的「优美的排列」的数量。
说明:
-
优美的排列:假设有
$1 \sim n$ 的$n$ 个整数。如果用这些整数构造一个数组$perm$ (下标从$1$ 开始),使得数组第$i$ 位元素$perm[i]$ 满足下面两个条件之一,则该数组就是一个「优美的排列」:-
$perm[i]$ 能够被$i$ 整除; -
$i$ 能够被$perm[i]$ 整除。
-
-
$1 \le n \le 15$ 。
示例:
- 示例 1:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除
- 示例 2:
输入:n = 1
输出:1
这道题可以看做是「0046. 全排列」的升级版。
- 通过回溯算法我们可以将数组的所有排列情况列举出来。
- 因为只有满足第
$i$ 位元素能被$i$ 整除,或者满足$i$ 能整除第$i$ 位元素的条件下才符合要求,所以我们可以进行剪枝操作,不再考虑不满足要求的情况。 - 最后回溯完输出方案数。
class Solution:
def countArrangement(self, n: int) -> int:
ans = 0
visited = set()
def backtracking(index):
nonlocal ans
if index == n + 1:
ans += 1
return
for i in range(1, n + 1):
if i in visited:
continue
if i % index == 0 or index % i == 0:
visited.add(i)
backtracking(index + 1)
visited.remove(i)
backtracking(1)
return ans
-
时间复杂度:$O(n!)$,其中
$n$ 为给定整数。 -
空间复杂度:$O(n)$,递归栈空间大小为
$O(n)$ 。
因为
「状态压缩」指的是使用一个
举个例子:
-
$n = 4, state = (1001)_2$ ,表示选择了数字$1$ 、$4$,剩余数字$2$ 和$3$ 未被选择。 -
$n = 6, state = (011010)_2$ ,表示选择了数字$2$ 、$4$、$5$,剩余数字$1$ 、$3$、$6$ 未被选择。
这样我们就可以使用
如果我们需要检查值为
如果为
按照排列的数字个数、数字集合的选择情况进行阶段划分。
定义状态
假设
那么
所以状态转移方程为:$dp[i][state] = \sum_{k = 1}^n dp[i - 1][state \text{ & } (\neg(1 \text{ <} \text{< } (k - 1)))]$。
- 不考虑任何数($i = 0, state = 0$)的情况下,方案数为
$1$ 。
根据我们之前定义的状态,$dp[i][state]$ 表示为:考虑前
class Solution:
def countArrangement(self, n: int) -> int:
states = 1 << n
dp = [[0 for _ in range(states)] for _ in range(n + 1)]
dp[0][0] = 1
for i in range(1, n + 1): # 枚举第 i 个位置
for state in range(states): # 枚举所有状态
one_num = bin(state).count("1") # 计算当前状态中选择了多少个数字(即统计 1 的个数)
if one_num != i: # 只有 i 与选择数字个数相同时才能计算
continue
for k in range(1, n + 1): # 枚举第 i 个位置(最后 1 位)上所选的数字
if state >> (k - 1) & 1 == 0: # 只有 state 第 k 个位置上为 1 才表示选了该数字
continue
if k % i == 0 or i % k == 0: # 只有满足整除关系才符合要求
# dp[i][state] 由前 i - 1 个位置,且 state 第 k 位为 0 的状态而来
dp[i][state] += dp[i - 1][state & (~(1 << (k - 1)))]
return dp[i][states - 1]
-
时间复杂度:$O(n^2 \times 2^n)$,其中
$n$ 为给定整数。 - 空间复杂度:$O(n \times 2^n)$。
通过二维的「状态压缩 DP」可以看出,当我们在考虑第
而我们可以根据
而这样,我们还可以进一步优化状态的定义,将二维的状态优化为一维的状态。具体做法如下:
按照数字集合的选择情况进行阶段划分。
定义状态
对于状态
则
所以状态转移方程为:$dp[state] = \sum_{k = 1}^n dp[state \oplus (1 << (k - 1))]$
- 不考虑任何数的情况下,方案数为
$1$ ,即:$dp[0] = 1$。
根据我们之前定义的状态,$dp[state]$ 表示为:当数字集合选择状态为
class Solution:
def countArrangement(self, n: int) -> int:
states = 1 << n
dp = [0 for _ in range(states)]
dp[0] = 1
for state in range(states): # 枚举所有状态
one_num = bin(state).count("1") # 计算当前状态中选择了多少个数字(即统计 1 的个数)
for k in range(1, n + 1): # 枚举最后 1 位上所选的数字
if state >> (k - 1) & 1 == 0: # 只有 state 第 k 个位置上为 1 才表示选了该数字
continue
if one_num % k == 0 or k % one_num == 0: # 只有满足整除关系才符合要求
# dp[state] 由前 one_num - 1 个位置,且 state 第 k 位为 0 的状态而来
dp[state] += dp[state ^ (1 << (k - 1))]
return dp[states - 1]
-
时间复杂度:$O(n \times 2^n)$,其中
$n$ 为给定整数。 - 空间复杂度:$O(2^n)$。