目录
知识点
位运算:
- 概念:对二进制位进行操作的运算。比如代码中的
(1 << n)
表示将数字 1 左移n
位,得到一个整数,其在二进制表示下只有第n + 1
位为 1,其他位为 0。 - 基本用法:包括左移(
<<
)、右移(>>
)、与(&
)、或(|
)、异或(^
)等操作。
动态规划(结合记忆化搜索):
- 概念:将一个复杂问题分解为若干个子问题,通过求解子问题并保存结果,避免重复计算,从而高效解决原问题。
- 基本用法:
- 定义状态:如这里的
memo[s][i]
表示在某种状态s
和选择了元素i
时的结果。 - 状态转移方程:即如何从已有的状态计算出其他状态,如代码中根据不同条件计算
res
并更新记忆数组。
- 定义状态:如这里的
思路
dp[1 << 0][0] = dp[1][0] = 1 (表示选择数字 2 的方案数为 1 )
dp[1 << 1][1] = dp[2][1] = 1 (表示选择数字 3 的方案数为 1 )
dp[1 << 2][2] = dp[4][2] = 1 (表示选择数字 6 的方案数为 1 )
解题方法
dp[u][j] 表示当前状态下的方案数,其中 u 是一个二进制数,表示当前已经选择的数字的集合(通过位运算表示状态压缩),j 表示最后一次选择的数字的索引。
- 初始化时,对于每个单独的数字 i(即只包含数字 i 的集合),将 dp[1 << i][i] 设置为 1,表示只有一个数字的排列方案数为 1。
- 然后通过两层循环遍历所有可能的状态 u 和数字索引 j 。如果数字 j 不在当前集合 u 中(通过位运算判断),则再通过内层的循环遍历之前已经选择过的数字 k(在集合 u 中)。如果数字 k 和数字 j 满足题目中特别排列的条件(要么 nums[j] % nums[k] == 0 要么 nums[k] % nums[j] == 0 ),则更新状态 dp[u | (1 << j)][j],即加入数字 j 后的方案数,等于原来的方案数加上之前状态 dp[u][k] 的方案数,并对结果取模防止溢出。
- 最后,通过遍历所有可能的最后一次选择的数字 j,计算所有满足条件的排列方案数的总和,并对总和取模得到最终答案。
时间复杂度
O(n2∗2)
空间复杂度
𝑂(𝑛∗2𝑛)O(n∗2n)
Code
class Solution {
public int specialPerm(int[] nums) {
int mod = (int) 1e9 + 7;
int n = nums.length;
// dp[u][j] := 当前集合为 u,最后一次选择为 j 时的特殊排列方案数
int[][] dp = new int[1 << n][n];
for (int i = 0; i < n; i++) {
// 选择 i 后,集合中增加 i
dp[1 << i][i] = 1;
}
for (int u = 0; u < (1 << n); u++) {
// 考虑当前集合为 u 时,加入新的数到集合中
for (int j = 0; j < n; j++) {
// 集合 u 中不存在 j 时,j 才能加入集合
if ((u >> j & 1) == 0) {
// 判断上一个选择的数 k 是否与 j 满足题意要求
for (int k = 0; k < n; k++) {
// k 不在集合中时,说明上一个数选择的数不可能是 k
if ((u & (1 << k)) > 0) {
// k 和 j 满足题意要求时才能形成特殊排列
if ((nums[j] % nums[k] == 0 || nums[k] % nums[j] == 0)) {
dp[u | (1 << j)][j] = (dp[u | (1 << j)][j] + dp[u][k]) % mod;
}
}
}
}
}
}
int sum = 0;
// 集合为 nums,遍历最后一次选择的数
for (int j = 0; j < n; j++) {
sum = (sum + dp[(1 << n) - 1][j]) % mod;
}
return sum;
}
}