题目
给你一个下标从 0
开始的整数数组 nums
,它包含 n
个互不相同的正整数。如果 nums
的一个排列满足以下条件,我们称它是一个特别的排列:
对于 0 <= i < n - 1
的下标 i
,要么 nums[i] % nums[i+1] == 0
,要么 nums[i+1] % nums[i] == 0
。
请你返回特别排列的总数目,由于答案可能很大,请将它对 10^9 + 7
取余后返回。
示例 1:
输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。
示例 2:
输入:nums = [1,4,3]
输出:2
解释:[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。
提示:
2 <= nums.length <= 14
1 <= nums[i] <= 10^9
代码
完整代码
#include <stdbool.h>
#include <stdio.h>
// 深度优先搜索(DFS)函数
void dfs(int* nums, int numsSize, int* cnt, bool* isUsed, int* arr, int index)
{
if(index == numsSize) // 找到一种排列
{
(*cnt)++;
return;
}
for (int i = 0; i < numsSize; i++)
{
if(isUsed[i])
{
continue;
}
if(arr[index - 1] % nums[i] == 0 || nums[i] % arr[index - 1] == 0)
{
arr[index] = nums[i];
isUsed[i] = true;
dfs(nums, numsSize, cnt, isUsed, arr, index+1);
isUsed[i] = false;
}
}
return;
}
// 主函数
int specialPerm(int* nums, int numsSize){
if(numsSize == 2)
{
if(nums[0] % nums[1] == 0 || nums[1] % nums[0] == 0)
{
return 2;
}
return 0;
}
bool *isUsed = (bool*)calloc(numsSize, sizeof(bool));
int *arr = (int*)calloc(numsSize, sizeof(int));
int cnt = 0;
for (int i = 0; i < numsSize; i++)
{
arr[0] = nums[i]; // 每个数字做起始依一次
isUsed[i] = true;
dfs(nums, numsSize, &cnt, isUsed, arr, 1);
isUsed[i] = false;
}
free(isUsed);
free(arr);
return cnt % (1000000000+7);
}
思路分析
这套代码用了深度优先搜索(DFS)的方法。
整体思路是通过枚举所有可能的排列,检查每一个排列是否满足特别排列的条件。
拆解分析
- 深度优先搜索(DFS)函数
void dfs(int* nums, int numsSize, int* cnt, bool* isUsed, int* arr, int index)
{
if(index == numsSize) // 找到一种排列
{
(*cnt)++;
return;
}
for (int i = 0; i < numsSize; i++)
{
if(isUsed[i])
{
continue;
}
if(arr[index - 1] % nums[i] == 0 || nums[i] % arr[index - 1] == 0)
{
arr[index] = nums[i];
isUsed[i] = true;
dfs(nums, numsSize, cnt, isUsed, arr, index+1);
isUsed[i] = false;
}
}
return;
}
DFS 函数用于生成所有可能的排列,并检查每一个排列是否满足特别排列的条件。index
表示当前排列的下标,cnt
用于记录满足条件的排列数量,isUsed
数组用于标记某个元素是否已被使用,arr
用于存储当前的排列。
- 主函数
int specialPerm(int* nums, int numsSize){
if(numsSize == 2)
{
if(nums[0] % nums[1] == 0 || nums[1] % nums[0] == 0)
{
return 2;
}
return 0;
}
bool *isUsed = (bool*)calloc(numsSize, sizeof(bool));
int *arr = (int*)calloc(numsSize, sizeof(int));
int cnt = 0;
for (int i = 0; i < numsSize; i++)
{
arr[0] = nums[i]; // 每个数字做起始依一次
isUsed[i] = true;
dfs(nums, numsSize, &cnt, isUsed, arr, 1);
isUsed[i] = false;
}
free(isUsed);
free(arr);
return cnt % (1000000000+7);
}
主函数首先处理特殊情况,当数组大小为 2
时,直接判断两元素是否互为倍数。然后使用 DFS 方法枚举所有排列,统计满足条件的排列数目,并返回对 10^9 + 7
取余后的结果。
复杂度分析
- 时间复杂度:由于使用 DFS 方法生成所有排列,对于
n
个元素,排列的总数为n!
。在每一步中检查两个元素的倍数关系,因此时间复杂度为O(n! * n)
。 - 空间复杂度:使用了额外的数组
isUsed
和arr
,每个数组的空间复杂度为O(n)
。
一题多解
动态规划(DP)解法
完整代码
#include <stdio.h>
#include <stdbool.h>
// 动态规划解法
int specialPermDP(int* nums, int numsSize) {
int MOD = 1000000007;
int dp[1 << numsSize][numsSize];
memset(dp, 0, sizeof(dp));
for (int i = 0; i < numsSize; i++) {
dp[1 << i][i] = 1;
}
for (int mask = 1; mask < (1 << numsSize); mask++) {
for (int i = 0; i < numsSize; i++) {
if ((mask & (1 << i)) == 0) continue;
for (int j = 0; j < numsSize; j++) {
if (mask & (1 << j)) continue;
if (nums[i] % nums[j] == 0 || nums[j] % nums[i] == 0) {
dp[mask | (1 << j)][j] = (dp[mask | (1 << j)][j] + dp[mask][i]) % MOD;
}
}
}
}
int result = 0;
for (int i = 0; i < numsSize; i++) {
result = (result + dp[(1 << numsSize) - 1][i]) % MOD;
}
return result;
}
思路分析
这个解法使用了动态规划(DP)的方法。通过掩码 mask
来表示当前排列的状态,dp[mask][i]
表示当前排列状态为 mask
,最后一个元素是 nums[i]
的排列数。
拆解分析
1. 初始化状态
在动态规划解法中,我们首先初始化每个元素作为起始元素的状态:
for (int i = 0; i < numsSize; i++) {
dp[1 << i][i] = 1;
}
这段代码的意思是,对于每个元素 nums[i]
,我们将其作为排列的起始元素,这样的排列数初始为 1
。这里 1 << i
表示一个掩码,其中只有第 i
位是 1
,表示元素 nums[i]
已经被使用。
2. 状态转移
动态规划的核心部分是状态转移。我们使用一个掩码 mask
来表示当前排列的状态,其中掩码的第 i
位为 1
表示元素 nums[i]
已经被使用。dp[mask][i]
表示当前排列状态为 mask
,且最后一个元素是 nums[i]
的排列数。
我们遍历所有可能的状态和元素,通过以下代码更新状态转移:
for (int mask = 1; mask < (1 << numsSize); mask++) { // 遍历所有可能的状态
for (int i = 0; i < numsSize; i++) { // 遍历所有元素作为当前排列的最后一个元素
if ((mask & (1 << i)) == 0) continue; // 如果当前元素没有被使用,跳过
for (int j = 0; j < numsSize; j++) { // 尝试将每个元素加入到当前排列的末尾
if (mask & (1 << j)) continue; // 如果元素已经被使用,跳过
if (nums[i] % nums[j] == 0 || nums[j] % nums[i] == 0) { // 检查是否满足条件
dp[mask | (1 << j)][j] = (dp[mask | (1 << j)][j] + dp[mask][i]) % MOD;
}
}
}
}
for (int mask = 1; mask < (1 << numsSize); mask++)
:遍历所有可能的状态,mask
的范围是1
到2^numsSize - 1
。每个状态用一个二进制数表示,例如mask = 5
对应的二进制是0101
,表示第0
和第2
个元素已被使用。for (int i = 0; i < numsSize; i++)
:遍历所有元素作为当前排列的最后一个元素。if ((mask & (1 << i)) == 0) continue
:如果当前元素没有被使用,跳过。for (int j = 0; j < numsSize; j++)
:尝试将每个元素加入到当前排列的末尾。if (mask & (1 << j)) continue
:如果元素已经被使用,跳过。if (nums[i] % nums[j] == 0 || nums[j] % nums[i] == 0)
:检查nums[i]
和nums[j]
是否满足特别排列的条件。dp[mask | (1 << j)][j] = (dp[mask | (1 << j)][j] + dp[mask][i]) % MOD
:如果满足条件,则更新状态dp[mask | (1 << j)][j]
,表示将nums[j]
加入到当前排列末尾后的新状态。
3. 计算结果
最终结果为所有状态为 (1 << numsSize) - 1
且最后一个元素为 i
的排列数之和:
int result = 0;
for (int i = 0; i < numsSize; i++) {
result = (result + dp[(1 << numsSize) - 1][i]) % MOD;
}
for (int i = 0; i < numsSize; i++)
:遍历所有元素作为最后一个元素的排列。result = (result + dp[(1 << numsSize) - 1][i]) % MOD
:将所有状态为(1 << numsSize) - 1
(即所有元素都被使用)且最后一个元素为i
的排列数累加到结果中,并对10^9 + 7
取余。
通过上述过程,我们可以计算出所有满足条件的特别排列的总数目。
复杂度分析
- 时间复杂度:动态规划状态数为
2^n
,每个状态的转移时间为O(n^2)
,因此总时间复杂度为O(n^2 * 2^n)
。 - 空间复杂度:使用了
dp
数组,空间复杂度为O(n * 2^n)
。
结果
DFS 和 DP 是两种常见的解决排列组合问题的方法。DFS 通过递归遍历所有可能的排列,DP 通过状态转移记录每个状态的排列数。两种方法在不同的场景下有不同的优势和劣势。
DFS:
O(n!*n)
还是不彳亍
DP:
彳亍