Leetcode 第 345 场周赛 Problem C 特别的排列(状压 DP + 记忆化搜索)

  • Leetcode 第 345 场周赛 Problem C 特别的排列
  • 题目
    • 给你一个下标从 0 开始的整数数组 nums ,它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件,我们称它是一个特别的排列:
    • 对于 0 <= i < n - 1 的下标 i ,要么 nums[i] % nums[i+1] == 0 ,要么 nums[i+1] % nums[i] == 0 。
    • 请你返回特别排列的总数目,由于答案可能很大,请将它对 10^9 + 7 取余 后返回。
    • 2 <= nums.length <= 14
    • 1 <= nums[i] <= 10^9
  • 解法一
    • 状压 DP + 记忆化搜索:首先思考暴力解法,就是每一位去放置不同的数字,同时校验该位与前一位要满足正向或反向余数为 0,此时可以考虑记忆化优化,
    • 定义状态(记忆化存储的值):dp[i] 中 i 为转化为二进制后存在哪些元素(如 5=101,代表存在下标 0、3 的元素,其他下标不存在),它的值为包含多少排列数,然后类似分支法将大区间拆分成小区间求值,如 dp[i]=dp[i-j]*dp[j],同时还要满足前一个区间的尾与下一个区间的头余数为 0,因此 dp[i][j][k] 代表以 j 为头、k 为尾,
    • 初始化:可以将需要求解的数初始化为 -1,只有一种情况的数初始化为 1,其他数初始化为 0(相当于这种情况结果加 0),如果不小于 0 就直接返回
      • 如果 i 等于 0 代表没有任何数结果一定为 0
      • 如果 i 在二进制下仅有一个 1,则 j 与 k 相等、同时 i 对应二进制下的 1 的位置就是 j/k,此时结果为 1,其他情况为 0
      • 如果 i 在二进制下超过一个 1,则 j 与 k 不能相同、同时 i 对应二进制下的 1 包含 j 与 k,此时结果需要求出为 -1,其他情况为 0
    • 动规转移方程:dp[i][j][k] = dp[i-(1<<k)][j][l] * dp[1<<k][k][k](nums[l] 是与 nums[k] 余数为 0 的一系列数,同时 l 必须存在 i 转化为的二进制位中)
    • 结果:当 i 在二进制下所有值都是 1 的情况下,所有的 j 与 k 的总和
    • 时间复杂度:O(2n*n2),空间复杂度:O(2n*n2)
  • 代码一
    /**
     * 状压 DP + 记忆化搜索:首先思考暴力解法,就是每一位去放置不同的数字,同时校验该位与前一位要满足正向或反向余数为 0,此时可以考虑记忆化优化,
     * 定义状态(记忆化存储的值):dp[i] 中 i 为转化为二进制后存在哪些元素(如 5=101,代表存在下标 0、3 的元素,其他下标不存在),它的值为包含多少排列数,
     * 然后类似分支法将大区间拆分成小区间求值,如 dp[i]=dp[i-j]*dp[j],同时还要满足前一个区间的尾与下一个区间的头余数为 0,因此 dp[i][j][k] 代表以 j 为头、k 为尾,
     * 初始化:可以将需要求解的数初始化为 -1,只有一种情况的数初始化为 1,其他数初始化为 0(相当于这种情况结果加 0),如果不小于 0 就直接返回
     *     如果 i 等于 0 代表没有任何数结果一定为 0
     *     如果 i 在二进制下仅有一个 1,则 j 与 k 相等、同时 i 对应二进制下的 1 的位置就是 j/k,此时结果为 1,其他情况为 0
     *     如果 i 在二进制下超过一个 1,则 j 与 k 不能相同、同时 i 对应二进制下的 1 包含 j 与 k,此时结果需要求出为 -1,其他情况为 0
     * 动规转移方程:dp[i][j][k] = dp[i-(1<<k)][j][l] * dp[1<<k][k][k](nums[l] 是与 nums[k] 余数为 0 的一系列数,同时 l 必须存在 i 转化为的二进制位中)
     * 结果:当 i 在二进制下所有值都是 1 的情况下,所有的 j 与 k 的总和
     * 时间复杂度:O(2^n*n^2),空间复杂度:O(2^n*n^2)
     */
    public int solution(int[] nums) {
    
    
        int mod = 1_000_000_007;

        int len = nums.length;
        // 状压所有元素的最大值(全为 1 的值)
        int permTotal = (1 << len) - 1;

        // 定义状态并初始化 dp
        long[][][] dp = initDp(permTotal + 1, len);

        // 创建二维数组快速获取 nums[i] 与 nums[j] 是否正向/反向互除为 0(i != j)
        boolean[][] special = initSpecial(nums, len);

        // 当 i 在二进制下所有值都是 1 的情况下,所有的 j 与 k 的总和
        return doSpecialPerm(nums, len, dp, permTotal, special, mod);
    }

    /**
     * 定义状态并初始化 dp
     */
    private long[][][] initDp(int permTotal, int len) {
    
    
        long[][][] dp = new long[permTotal][len][len];

        for (int i = 1; i < permTotal; i++) {
    
    
            for (int j = 0; j < len; j++) {
    
    
                for (int k = 0; k < len; k++) {
    
    
                    // i 在二进制下仅有一个 1
                    if (i - (lowbit(i)) == 0) {
    
    
                        // i 对应二进制下的 1 的位置就是 j/k
                        if (j == k && (1 << j) == i) {
    
    
                            // 仅有这一种情况
                            dp[i][j][k] = 1;
                        }
                    // i 在二进制下超过一个 1
                    } else {
    
    
                        // i 对应二进制下的 1 包含 j 与 k
                        if (j != k && (i & (1 << j)) > 0 && (i & (1 << k)) > 0) {
    
    
                            // 这些才是超过一个 1 并且符合条件的情况(需要求解,不符合条件默认赋值为 0)
                            dp[i][j][k] = -1;
                        }
                    }
                }
            }
        }

//        System.out.println(permTotal);
//        for (int i = 1; i < permTotal; i++) {
    
    
//            for (int j = 0; j < len; j++) {
    
    
//                for (int k = 0; k < len; k++) {
    
    
//                    System.out.print(i + ":" + j + ":" + k + ":" + dp[i][j][k] + " ");
//                }
//            }
//            System.out.println();
//        }

        return dp;
    }

    /**
     * 返回 i 二进制下最后一个 1 形成的值、即将最后一个 1 前面的值删除
     */
    private int lowbit(int i) {
    
    
        return i & -i;
    }

    /**
     * 创建二维数组快速获取 nums[i] 与 nums[j] 是否正向/反向互除为 0(i != j)
     */
    private boolean[][] initSpecial(int[] nums, int len) {
    
    
        boolean[][] special = new boolean[len][len];
        for (int i = 0; i < len; i++) {
    
    
            for (int j = 0; j < len; j++) {
    
    
                if (i != j && (nums[i] % nums[j] == 0 || nums[j] % nums[i] == 0)) {
    
    
                    special[i][j] = true;
                }
            }
        }
        return special;
    }

    /**
     * 当 i 在二进制下所有值都是 1 的情况下,所有的 j 与 k 的总和
     */
    private int doSpecialPerm(int[] nums, int len, long[][][] dp, int permTotal, boolean[][] special, int mod) {
    
    
        int res = 0;
        for (int j = 0; j < len; j++) {
    
    
            for (int k = 0; k < len; k++) {
    
    
                if (j == k) {
    
    
                    continue;
                }
                // 递归 + 记忆化搜索
                dfs(dp, permTotal, j, k, len, special, mod);
//                System.out.println(j + " : " + k + " : " + dp[permTotal][j][k]);
                res += dp[permTotal][j][k];
                res %= mod;
            }
        }
        return res;
    }

    /**
     * 递归 + 记忆化搜索
     */
    private long dfs(long[][][] dp, int perm, int start, int end, int len, boolean[][] special, int mod) {
    
    
        if (dp[perm][start][end] >= 0) {
    
    
            return dp[perm][start][end];
        }

        long res = 0;
        for (int i = 0; i < len; i++) {
    
    
            // nums[i] 是与 nums[end] 余数为 0 的一系列数,同时 l 必须存在 i 转化为的二进制位中、否则返回 0
            if (special[i][end]) {
    
    
                res += dfs(dp, perm - (1 << end), start, i, len, special, mod) * dp[1 << end][end][end];
                res %= mod;
            }
        }
        // 记忆化
        dp[perm][start][end] = res;

        return dp[perm][start][end];
    }
  • 解法二
    • 在上述基础上,由于 dp[1<<k][k][k] 其实就是 1,同时可以看做每次添加一个元素,因此可以省掉一维
    • 定义状态:dp[i][j] 中 i 定义不变,j 代表最后一个元素为 nums[j] 的排列数
    • 初始化: 所有元素为 0
    • 转移方程:当 i 的二进制仅有一个 1 时、仅 dp[i][logi] = 1 其他为 0,
    • 否则 dp[i][j] += dp[i-(1<<l)][l] nums[l] 是与 nums[j] 余数为 0 的一系列数,同时 l 必须存在 i 转化为的二进制位中
    • 结果:当 i 在二进制下所有值都是 1 的情况下,所有 j 之和
    • 时间复杂度:O(2n*n2),空间复杂度:O(2^n*n)
  • 代码二
    /**
     * 在上述基础上,由于 dp[1<<k][k][k] 其实就是 1,同时可以看做每次添加一个元素,因此可以省掉一维
     * 定义状态:dp[i][j] 中 i 定义不变,j 代表最后一个元素为 nums[j] 的排列数
     * 初始化: 所有元素为 0
     * 转移方程:当 i 的二进制仅有一个 1 时、仅 dp[i][logi] = 1 其他为 0,
     * 否则 dp[i][j] += dp[i-(1<<l)][l] nums[l] 是与 nums[j] 余数为 0 的一系列数,同时 l 必须存在 i 转化为的二进制位中
     * 结果:当 i 在二进制下所有值都是 1 的情况下,所有 j 之和
     * 时间复杂度:O(2^n*n^2),空间复杂度:O(2^n*n)
     */
    private int solutionOptimization(int[] nums) {
    
    
        int mod = 1_000_000_007;

        int len = nums.length;
        // 与每一位 i 余数为 0 的下标存入 Set[]
        Set<Integer>[] specialSet = getSpecialSet(nums, len);
//        System.out.println(Arrays.toString(specialSet));

        // 将二进制的仅一位 1 的值存入 key、1 存在第几位存入 value(从 0 开始)
        Map<Integer, Integer> binaryIndexMap = getBinaryIndexMap(len);
//        System.out.println(binaryIndexMap);

        // 状压所有元素的最大值(全为 1 的值)
        int permTotal = (1 << len) - 1;

        // 定义与初始化 DP
        int[][] dp = new int[permTotal + 1][len];
        dp[1][0] = 1;

        int res = 0;
        // 循环进行状态转移
        for (int i = 1; i < permTotal + 1; i++) {
    
    
            // 当 i 的二进制仅有一个 1 时
            if (i - lowbit(i) == 0) {
    
    
                dp[i][binaryIndexMap.get(i)] = 1;

            } else {
    
    

                for (int j = 0; j < len; j++) {
    
    
                    // j 必须存在 i 转化为的二进制位中
                    if ((i & (1 << j)) > 0) {
    
    
                        // nums[k] 是与 nums[j] 余数为 0 的一系列数
                        for (Integer special : specialSet[j]) {
    
    
                            // special 必须存在 i 转化为的二进制位中
                            if ((i & (1 << special)) > 0) {
    
    
                                dp[i][j] = (dp[i][j] + dp[i - (1 << j)][special]) % mod;
                            }
                        }
                    }
                    if (i == permTotal) {
    
    
                        res = (res + dp[i][j]) % mod;
                    }
                }
            }
        }

//        for (int i = 0; i < dp.length; i++) {
    
    
//            System.out.println(Arrays.toString(dp[i]));
//        }

        return res;
    }

    /**
     * 将二进制的仅一位 1 的值存入 key、1 存在第几位存入 value(从 0 开始)
     */
    private Map<Integer,Integer> getBinaryIndexMap(int total) {
    
    
        Map<Integer, Integer> binaryIndexMap = new HashMap<>();

        int key = 1;
        for (int i = 0; i < total; i++) {
    
    
            binaryIndexMap.put(key, i);
            key <<= 1;
        }

        return binaryIndexMap;
    }


    /**
     * 与每一位 i 余数为 0 的下标存入 Set[]
     */
    private Set<Integer>[] getSpecialSet(int[] nums, int len) {
    
    
        Set<Integer>[] specialSet = new HashSet[len];
        for (int i = 0; i < len; i++) {
    
    
            specialSet[i] = new HashSet<Integer>();
        }

        for (int i = 0; i < len; i++) {
    
    
            for (int j = 0; j < len; j++) {
    
    
                if (i != j && (nums[i] % nums[j] == 0 || nums[j] % nums[i] == 0)) {
    
    
                    specialSet[i].add(j);
                }
            }
        }

        return specialSet;
    }
    /**
     * 返回 i 二进制下最后一个 1 形成的值、即将最后一个 1 前面的值删除
     */
    private int lowbit(int i) {
    
    
        return i & -i;
    }

猜你喜欢

转载自blog.csdn.net/qq_33530115/article/details/131276898