前言
前两题很简单。
第三题 特别的排列
题目
给你一个下标从 0 开始的整数数组 nums ,它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件,我们称它是一个特别的排列:
对于 0 <= i < n - 1 的下标 i ,要么 nums[i] % nums[i+1] == 0 ,要么 nums[i+1] % nums[i] == 0 。
请你返回特别排列的总数目,由于答案可能很大,请将它对 109 + 7 取余 后返回。
示例 1:
输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。
题解
1、回溯方法(超时)
思路
求排列总数的问题,最直接的办法就是使用回溯进行全排列,列举出所有的可能。
做法
- 前置:首先二维遍历一遍数组,对每一个第i位上的数,得到一个由所有满足nums[j] % nums[i] == 0的j组成的列表positiveList,将(i, positiveList)存在哈希表positive中;同理将所有满足nums[k] % nums[i] == 0 的k组成的列表negtiveList当作value, i当作key存在哈希表negtive中;
potive = {(i, [j/nums[j] % nums[i] == 0]), i.e., (3, [6, 9])},
negative = {(i, [k/nums[i] % nums[k] = 0]), i.e., (6, [2, 3])};
- 递归过程中,维护一个变量index表示当前已确定的位数,维护一个变量lastSorted表示排列中上一位数在原始数组中的坐标,同时维护一个集合或者哈希表map表示前面轮递归选中的是哪些数;在每一轮递归中,我们从哈希表中拿到lastSorted为key得到的positiveList和negtiveList,遍历两个列表的元素k,如果map中k的值为false说明前面轮递归中没有出现过,这一轮可以被选中,更新lastSorted为k和index为index + 1, 继续递归;
- 递归终止条件是列表的位数只剩最后一位待确定,即index == n - 2,此时对当前的lastSorted做一轮递归,每得到一个满足条件的k就将返回值加1;
代码:
class Solution {
Map<Integer, List<Integer>> positive;
Map<Integer, List<Integer>> negtive;
long res;
long MOD = (long)Math.pow(10, 9) + 7;
int n;
public int specialPerm(int[] nums) {
//(i, [j/nums[j] % nums[i] == 0]), i.e., (3, [6, 9])
positive = new HashMap<>();
//(i, [k/nums[i] % nums[k] = 0]), i.e., (6, [2, 3])
negtive = new HashMap<>();
res = 0l;
n = nums.length;
Map<Integer, Boolean> map = new HashMap<>();
for (int i = 0; i < n; ++i) {
positive.put(nums[i], new ArrayList<>());
negtive.put(nums[i], new ArrayList<>());
map.put(nums[i], false);
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (j == i) {
continue;
}
if (nums[j] % nums[i] == 0) {
List<Integer> positiveList = positive.getOrDefault(nums[i], new ArrayList<>());
positiveList.add(nums[j]);
positive.put(nums[i], positiveList);
List<Integer> negtiveList = negtive.getOrDefault(nums[j], new ArrayList<>());
negtiveList.add(nums[i]);
negtive.put(nums[j], negtiveList);
}
}
}
for (int i = 0; i < n; ++i) {
map.put(nums[i], true);
dfs(nums[i], 0, map);
map.put(nums[i], false);
}
return (int)res;
}
//回溯, 维护lastSorted作为当前排列的最后一位在nums中的坐标, map表示nums中哪些已被排列
private void dfs(int lastSorted, int index, Map<Integer, Boolean> map) {
List<Integer> posList = positive.get(lastSorted);
List<Integer> negList = negtive.get(lastSorted);
if (index == n - 2) {
int now = 0;
for (Integer i : posList) {
if (map.get(i)) {
continue;
}
now++;
}
for (Integer i : negList) {
if (map.get(i)) {
continue;
}
now++;
}
res = (res + now) % (MOD);
return;
}
for (Integer i : posList) {
if (map.get(i)) {
continue;
}
map.put(i, true);
dfs(i, index + 1, map);
map.put(i, false);
}
for (Integer i : negList) {
if (map.get(i)) {
continue;
}
map.put(i, true);
dfs(i, index + 1, map);
map.put(i, false);
}
}
}
2、回溯 + 状态压缩+记忆化搜索
思路
在第一种做法中,我们可以做一些优化;
从第一种解法可以发现,当找下一个数字入排列时,我们关心的只有两处
- 当前排列的最后一个值,用来做整除的校验;
- 当前排列包含的数字,除最后一个数字外不关心其他数字的具体顺序,因为对找下一个数字没有帮助;
第一点必须要维护一个变量没啥可优化的,围绕第二点做优化。
具体地,第二点我们可以通过维护一个数字集合实现,回溯做法里就是这样做的,但由于不关心具体顺序所以可以将集合通过里面的元素在原始数组里的位置使用位运算压缩为整数i来表示;
此外,当我们对于值相同的同一个组合(i, lastSorted),我们在回溯的过程中会反复出现多次(由不在意顺序决定),因此我们可以通过做一个缓存来实现记忆化搜索.
代码
class Solution {
int MOD = (int)Math.pow(10, 9) + 7;
Map<String, Integer> cache = new HashMap<>();
public int specialPerm(int[] nums) {
int n = nums.length;
//i表示状态压缩值, j表示当前已确定的最后一个值
int i = (1 << n) - 1;
int res = 0;
// dfs(i, j) = sum(dfs(i ^ (1 << k), k)), 其中k和j满足题目中的条件;
for (int j = 0; j < n; ++j) {
res = (res + dfsDP(i ^ (1 << j), j, n, nums) % MOD) % MOD;
}
return res;
}
private int dfsDP(int i, int j, int n, int[] nums) {
int res = 0;
if (cache.containsKey(i + "_" + j)) {
return cache.get(i + "_" + j);
}
if (i == 0) {
return 1;
}
// 去掉第一种解法的前置处理,直接做一个循环
for (int k = 0; k < n; ++k) {
if ((i >> k & 1) == 1 && (nums[k] % nums[j] == 0 || nums[j] % nums[k] == 0)) {
res = ((res + dfsDP(i ^ (1 << k), k, n, nums)) % MOD ) % MOD;
}
}
cache.put(i + "_" + j, res);
return res;
}
}
3、迭代做法--状压DP+位运算
思路
我们尝试通过挖掘状态转移关系将递归转化为迭代来做,和第二种方法里一样,由于不关心具体顺序所以可以将集合通过里面的元素在原始数组里的位置使用位运算压缩为整数i来表示;
对于第一点我们仍然维护一个变量lastSorted,简化为j表示;
这时,我们使用f[i, j]表示当前排列数字状压值为i,最后一个元素为j时的所有满足条件的完整列个数,我们可以发现:
f[i, j] = sum(k, f[i ^ (1 << k), k] / nums[k] % nums[j] == 0 || nums[j] % nums[k] == 0),
即当k和j满足整除条件时,通过 i -> i ^ (1 << k)更新状压值,j -> k更新排列最后一个数字的值,形成一种动态规划关系。
初始条件为:状压值为0时,表示所有的数都已入列,此时对于所有的j均只有一种情况,即
f[0, j] = 1, 0 <= j < n
状压值最大为 ((1 << n - 1 ) ,此时队列为空;
最终结果为:排列大小为1,这个元素分别是初始数组里的每个数字时,f值的和,即
sum(j, f[(1 << n - 1) ^ (1 << j), j])/ 0 <= j < n;
代码
public int specialPerm(int[] nums) {
int n = nums.length;
//i表示状态压缩值, j表示当前已确定的最后一个值
int m = 1 << n;
int res = 0;
// 把递归转化为循环,自底向上: f(i, j) = sum(f(i ^ (1 << k), k)); f(0, j) = 1; 状态压缩值为i, 当前值为第j个
int[][] f = new int[m][n];
for (int j = 0; j < n; ++j) {
f[0][j] = 1;
}
for (int i = 1; i < m; ++i) {
for (int k = 0; k < n; ++k)
{
for (int j = 0; j < n; ++j) {
if (((i >> k) & 1) == 1 && (nums[k] % nums[j] == 0 || nums[j] % nums[k] == 0)) {
f[i][j] += f[i ^ (1 << k)][k] % MOD;
f[i][j] = f[i][j] % MOD;
}
}
}
}
for (int j = 0; j < n; ++j) {
res += f[(m - 1) ^ (1 << j)][j];
res = res % MOD;
}
return res;
}