leetcode
leetcode 2351 ~ 2400
特别的排列

特别的排列

难度:

标签:

题目描述

You are given a 0-indexed integer array nums containing n distinct positive integers. A permutation of nums is called special if:

  • For all indexes 0 <= i < n - 1, either nums[i] % nums[i+1] == 0 or nums[i+1] % nums[i] == 0.

Return the total number of special permutations. As the answer could be large, return it modulo 10+ 7.

 

Example 1:

Input: nums = [2,3,6]
Output: 2
Explanation: [3,6,2] and [2,6,3] are the two special permutations of nums.

Example 2:

Input: nums = [1,4,3]
Output: 2
Explanation: [3,1,4] and [4,1,3] are the two special permutations of nums.

 

Constraints:

  • 2 <= nums.length <= 14
  • 1 <= nums[i] <= 109

代码结果

运行时间: 305 ms, 内存: 29.2 MB


/*
 * 思路:
 * 1. 使用Java Stream API和递归来生成所有可能的排列。
 * 2. 对于每一个排列,检查是否满足条件,即对于0 <= i < n - 1,要么nums[i] % nums[i+1] == 0,要么nums[i+1] % nums[i] == 0。
 * 3. 统计所有满足条件的排列数目。
 * 4. 将结果对10^9 + 7取余。
 */

import java.util.*;
import java.util.stream.*;

public class SpecialPermutationsStream {
    private static final int MOD = 1000000007;

    public int specialPermutations(int[] nums) {
        return (int)permute(nums, 0).stream()
            .filter(this::isSpecialPermutation)
            .count() % MOD;
    }

    private List<List<Integer>> permute(int[] nums, int start) {
        if (start == nums.length) {
            return Collections.singletonList(Arrays.stream(nums).boxed().collect(Collectors.toList()));
        }
        List<List<Integer>> result = new ArrayList<>();
        for (int i = start; i < nums.length; i++) {
            swap(nums, start, i);
            result.addAll(permute(nums, start + 1));
            swap(nums, start, i);
        }
        return result;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    private boolean isSpecialPermutation(List<Integer> permutation) {
        for (int i = 0; i < permutation.size() - 1; i++) {
            int a = permutation.get(i);
            int b = permutation.get(i + 1);
            if (a % b != 0 && b % a != 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        SpecialPermutationsStream sps = new SpecialPermutationsStream();
        int[] nums1 = {2, 3, 6};
        int[] nums2 = {1, 4, 3};
        System.out.println(sps.specialPermutations(nums1)); // 输出:2
        System.out.println(sps.specialPermutations(nums2)); // 输出:2
    }
}

解释

方法:

该题解采用了动态规划与深度优先搜索(DFS)结合的方法来解决问题。首先对数组进行排序,利用排序的特性简化除法运算的条件检查。接着,构建一个图的表示方式,用一个整数数组g来表示节点之间的连接关系,其中g[i]的二进制中的第j位如果是1,表示nums[i]和nums[j]之间可以相互整除。然后,通过DFS遍历所有可能的排列组合,使用位掩码技术来表示当前已经选择的元素集合。最后,利用缓存机制(@cache)来优化重复计算,提高效率。

时间复杂度:

O(n * 2^n)

空间复杂度:

O(n + 2^n)

代码细节讲解

🦆
在题解中提到了使用排序来简化运算,排序后的数组具体如何简化除法条件的检查?
在数组排序之后,较小的数字会位于数组的前面,较大的数字则位于后面。这种排序可以简化除法条件的检查,因为对于任何两个数nums[i]和nums[j],如果i < j,那么nums[i]必然不大于nums[j]。这使得我们只需要检查nums[j]是否可以被nums[i]整除,而不必再检查nums[i]是否可以被nums[j]整除,从而减少了需要进行的除法运算次数,并提高了整体算法的效率。
🦆
题解中提到使用一个整数数组g来表示节点之间的连接关系,具体是如何使用二进制位来表示这种关系的?能否详细解释一下这种表示方法的原理和优势?
在题解中,每一个整数g[i]的二进制表示形式用来表示元素nums[i]与其他元素之间的关系。具体来说,如果g[i]的第j位是1(从0开始计数),这表示nums[i]可以与nums[j]相互整除。使用这种二进制位来表示连接关系的优势在于它极大地节省了空间,同时也方便了快速的位运算处理,如位与(&)、位或(|),以及位异或(^)。这种表示方法使得检查和更新节点之间关系的操作更加高效,特别是在需要频繁查询和修改节点间关系的图算法中。
🦆
题解中提到了查找端点,这里的端点是指什么?为什么端点的数量超过2会导致返回0?
在题解中,'端点'指的是那些在整除关系图中只与一个其他节点相连的节点。在二进制表示中,这样的端点的g[i]会恰好有两个1位(包括自身和连接的另一个节点)。如果一个图的端点数量超过两个,这意味着无法形成一个闭合的环或一条简单的链,这通常代表图不连贯或存在多个独立的子图,这不符合题目要求的特别排列条件。因此,如果端点数量超过两个,函数会直接返回0,表示不存在有效的排列方式。
🦆
DFS函数中使用了位掩码技术,位掩码在此算法中扮演了什么角色?如何利用位掩码来跟踪哪些元素已被选中?
在DFS函数中,位掩码用于有效地跟踪和管理在当前递归调用中已经被选中的元素。位掩码是一个整数,其中的每一位对应于nums数组中的一个元素。如果某位是1,这表示对应的元素已经被选中。在DFS递归过程中,每选择一个新元素,我们就通过位或运算将该元素对应的位设置为1。这种方法不仅减少了存储空间的需求(不需要额外的数组来跟踪选择状态),还使得操作如检查元素是否被选中(位与操作)和更新选择状态(位异或操作)变得非常快速和直接。

相关问题