从集合论到位运算——常见位运算技巧及相关习题

文章目录

  • 原文链接
    • 集合与集合
    • 集合与元素
    • 遍历集合
    • 枚举集合
  • 二进制基本原理(一张图片)
  • 题目练习
    • 位运算
      • 78. 子集
      • 77. 组合
      • 46. 全排列
    • 状态压缩DP
      • 2172. 数组的最大与和
        • 代码1——考虑放了的
        • 代码2——考虑没放的
      • 1125. 最小的必要团队⭐
      • 2305. 公平分发饼干⭐
      • 1494. 并行课程 II
      • LCP 53. 守护太空城
      • 1879. 两个数组最小的异或值之和
      • 1986. 完成任务的最少工作时间段

本文的内容主要是 马住 一些 关于 位运算的操作。

以及下面的练习题比较有质量。

原文链接

https://leetcode.cn/circle/discuss/CaOJ45/

从集合论到位运算——常见位运算技巧及相关习题_第1张图片

集合与集合

从集合论到位运算——常见位运算技巧及相关习题_第2张图片

集合与元素

从集合论到位运算——常见位运算技巧及相关习题_第3张图片
从集合论到位运算——常见位运算技巧及相关习题_第4张图片

遍历集合

for (int i = 0; i < n; i++) {
    if (((s >> i) & 1) == 1) { // i 在 s 中
        // 处理 i 的逻辑
    }
}

枚举集合

从集合论到位运算——常见位运算技巧及相关习题_第5张图片
从集合论到位运算——常见位运算技巧及相关习题_第6张图片
关于 Gosper’s Hack (生成 n元集合所有 k 元子集) 可见: 位运算技巧


二进制基本原理(一张图片)


题目练习

位运算

78. 子集

https://leetcode.cn/problems/subsets/

从集合论到位运算——常见位运算技巧及相关习题_第7张图片

可以用 mask 来存储结果。

class Solution {
    List<List<Integer>> ans = new ArrayList();
    int[] nums;

    public List<List<Integer>> subsets(int[] nums) {
        this.nums = nums;
        dfs(0, 0);
        return ans;
    }

    public void dfs(int i, int mask) {
        if (i == nums.length) {
            ans.add(op(mask));
            return;
        }
        dfs(i + 1, mask);
        dfs(i + 1, mask | (1 << (nums[i] + 10)));
    }

    public List<Integer> op(int mask) {
        List<Integer> res = new ArrayList<Integer>();
        while (mask != 0) {
            res.add(Integer.numberOfTrailingZeros(mask) - 10);
            mask &= mask - 1;
        }
        return res;
    }
}

77. 组合

https://leetcode.cn/problems/combinations/

从集合论到位运算——常见位运算技巧及相关习题_第8张图片

class Solution {
    List<List<Integer>> ans = new ArrayList();
    int n, k;

    public List<List<Integer>> combine(int n, int k) {
        this.n = n;
        this.k = k;
        dfs(1, 0);
        return ans;
    }

    public void dfs(int i, int mask) {
        if (i == n + 1 && Integer.bitCount(mask) == k) {
            ans.add(op(mask));
            return;
        }
        if (i > n || Integer.bitCount(mask) > k || Integer.bitCount(mask) + n - i + 1 < k) return;
        dfs(i + 1, mask);
        dfs(i + 1, mask | (1 << i));
    }

    public List<Integer> op(int mask) {
        List<Integer> res = new ArrayList<Integer>();
        while (mask != 0) {
            res.add(Integer.numberOfTrailingZeros(mask));
            mask &= mask - 1;
        }
        return res;
    }
}

46. 全排列

https://leetcode.cn/problems/permutations/

从集合论到位运算——常见位运算技巧及相关习题_第9张图片

用 mask 来记录各个数字是否已经被选择。

class Solution {
    List<List<Integer>> ans = new ArrayList();
    List<Integer> t = new ArrayList();
    int[] nums;
    public List<List<Integer>> permute(int[] nums) {
        this.nums = nums;
        dfs(0, 0);
        return ans;
    }

    public void dfs(int i, int mask) {
        if (i == nums.length) {
            ans.add(new ArrayList(t));
            return;
        }
        for (int j = 0; j < nums.length; ++j) {
            int v = nums[j] + 10;
            if ((mask >> v & 1) == 0) {
                t.add(nums[j]);
                dfs(i + 1, mask | (1 << v));
                t.remove(t.size() - 1);
            }
        }
    }
}

状态压缩DP

2172. 数组的最大与和

https://leetcode.cn/problems/maximum-and-sum-of-array/
从集合论到位运算——常见位运算技巧及相关习题_第10张图片

提示:
n == nums.length
1 <= numSlots <= 9
1 <= n <= 2 * numSlots
1 <= nums[i] <= 15

代码1——考虑放了的

class Solution {
    public int maximumANDSum(int[] nums, int numSlots) {
        int n = nums.length, ans = 0;
        // dp[i]表示组成集合 i 时的最大值
        int[] dp = new int[1 << (numSlots * 2)];

        for (int mask = 1; mask < dp.length; ++mask) {
            int c = Integer.bitCount(mask);
            if (c > n) continue;
            for (int i = 0; i < numSlots * 2; ++i) {    // 枚举每个篮子
                if ((mask >> i & 1) == 1) {             // 如果已经放了
                    dp[mask] = Math.max(dp[mask], dp[mask ^ (1 << i)] + (nums[c - 1] & (i / 2 + 1)));
                }
            }
            ans = Math.max(ans, dp[mask]);
        }
        return ans;
    }
}

代码2——考虑没放的

参见:【LeetCode周赛】2022上半年题目精选集——动态规划

1125. 最小的必要团队⭐

https://leetcode.cn/problems/smallest-sufficient-team/
从集合论到位运算——常见位运算技巧及相关习题_第11张图片

class Solution {
    public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
        int n = req_skills.length, m = people.size();
        // 字符串到索引的映射
        Map<String, Integer> map = new HashMap();
        for (int i = 0; i < n; ++i) {
            map.put(req_skills[i], i);
        }

        // 存储组成集合 i 时的最小人集合
        List<Integer>[] dp = new List[1 << n];
        dp[0] = new ArrayList<Integer>();
        for (int i = 0; i < m; ++i) {
            int curSkill = 0;       // 当前人员的能力
            for (String s: people.get(i)) curSkill |= 1 << map.get(s);

            for (int prev = 0; prev < 1 << n; ++prev) {
                if (dp[prev] == null) continue;     // 这种能力集合不能被组成就跳过
                int next = prev | curSkill;         // 加上当前人员之后的能力
                if (dp[next] == null || dp[next].size() > dp[prev].size() + 1) {
                    dp[next] = new ArrayList(dp[prev]);
                    dp[next].add(i);
                }
            } 
        }
        return dp[(1 << n) - 1].stream().mapToInt(Integer::intValue).toArray();
    }
}

TODO:我在官解的评论区写了自己错误的代码,等待有人解答哪里有错误。

2305. 公平分发饼干⭐

https://leetcode.cn/problems/fair-distribution-of-cookies/
从集合论到位运算——常见位运算技巧及相关习题_第12张图片

class Solution {
    public int distributeCookies(int[] cookies, int k) {
        int n = cookies.length;
        // dp[i][j]表示分给i个孩子j集合饼干时的小不公平程度
        int[][] dp = new int[k][1 << n];
        int[] sum = new int[1 << n];
        for (int i = 0; i < 1 << n; ++i) {
            for (int j = 0; j < n; ++j) {
                if ((i >> j & 1) == 1) sum[i] += cookies[j];
            }
        }
        dp[0] = sum;

        for (int j = 1; j < k; ++j) {       // 枚举j个学生的情况
            Arrays.fill(dp[j], 0x3f3f3f3f);
            for (int mask = 0; mask < 1 << n; ++mask) {     // 枚举每种饼干集合
                int c = Integer.bitCount(mask);     // 已经分了几个饼干
                for (int s = mask; s != 0; s = (s - 1) & mask) {    // 枚举mask的每个子集s
                    dp[j][mask] = Math.min(dp[j][mask], Math.max(sum[s], dp[j - 1][mask ^ s]));
                }
            }
        }
        return dp[k - 1][(1 << n) - 1];
    }
}

这道题目需要学会 枚举一个集合的所有子集 的方法。

1494. 并行课程 II

https://leetcode.cn/problems/parallel-courses-ii/
从集合论到位运算——常见位运算技巧及相关习题_第13张图片

枚举每个课程集合,计算该课程集合的前置课程集合。
那么当前可以学习的课程集合就是 当前课程集合去掉前置课程集合。

如果可以学习的课程集合 <= k 的话,那么全部都可以学习。
如果 > k 的话,就要枚举当前可以学习课程的所有子集,检查其子集是否 <= k,如果 是,则可以根据该状态更新答案。

class Solution {
    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        int[] pre = new int[1 << n];
        // 记录每个课程的先修课程集合
        for (int[] relation: relations) {
            pre[1 << (relation[1] - 1)] |= 1 << (relation[0] - 1);
        }

        // dp[i]表示学成集合i需要的最短时间
        int[] dp = new int[1 << n];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;

        for (int i = 1; i < (1 << n); ++i) {
            // 求当前的前置课程集合
            pre[i] = pre[i & (i - 1)] | pre[i & -i];  // 去掉最后一个1和取出最后一个1
            if ((pre[i] | i) != i) continue;            // i中有个前置课程没有学习
            int valid = i ^ pre[i];     // 当前可以学习的课程,也就是去掉所有已经学习过的前置课程
            if (Integer.bitCount(valid) <= k) {         // 全都可以学
                dp[i] = Math.min(dp[i], dp[i ^ valid] + 1);
            } else {                                    // 只能学其中k个
                for (int s = valid; s != 0; s = (s - 1) & valid) {  // 枚举valid的所有子集
                    if (Integer.bitCount(s) <= k) {     // 如果当前子集新学的课程 <= k 的话
                        dp[i] = Math.min(dp[i], dp[i ^ s] + 1);
                    }
                }
            }
        }

        return dp[dp.length - 1];
    }
}

LCP 53. 守护太空城

https://leetcode.cn/problems/EJvmW4/

从集合论到位运算——常见位运算技巧及相关习题_第14张图片

提示:
1 <= time.length == position.length <= 500
1 <= time[i] <= 5
0 <= position[i] <= 100

这道题挺难的。(超级难)

定义 dp[i][j] 表示考虑前 i 个舱室,且第 i 个舱室与第 i + 1 个舱室开启联合屏障的时间点集合为 j 时,所需的最小能量。

我们使用 union[i] 和 single[i] 分别记录开启 联合/单独 屏障的时间点集合恰好为 i 时,所需要的最少能量。

对于位置 0 ,联合保护罩的开启时间集合是 j ,则它的最小消耗就是 union[j] + single[((m - 1) ^ j) & rain[0]]。(即除去联合时间外,剩下且下雨的时间集合)

dp[i][j] 从 dp[i - 1][pre] 转移过来,其中 pre 是枚举 j 的补集。

class Solution {
    public int defendSpaceCity(int[] time, int[] position) {
        int n = Arrays.stream(position).max().getAsInt();
        int m = 1 << Arrays.stream(time).max().getAsInt();
        int[] rain = new int[n + 1];       // 记录每个位置的time集合
        for (int i = 0; i < time.length; ++i) {
            rain[position[i]] |= 1 << (time[i] - 1);
        }

        int[] union = new int[m];
        int[] single = new int[m];
        for (int i = 1; i < m; ++i) {       // 枚举time的集合
            int lb = i & -i, j = i ^ lb, lb2 = j & -j;
            union[i] = union[j] + (lb == (lb2 >> 1)? 1: 3);
            single[i] = single[j] + (lb == (lb2 >> 1)? 1: 2);
        }

        int[][] dp = new int[n + 1][m];
        // 初始化第0个舱室的开启联合屏障时间为j时的最小能量花费
        for (int j = 0; j < m; ++j) {
            // j集合时间联合,j之外且下雨的时间开单个
            dp[0][j] = union[j] + single[((m - 1) ^ j) & rain[0]];
        }
        for (int i = 1; i <= n; ++i) {
            Arrays.fill(dp[i], Integer.MAX_VALUE / 2);
            for (int j = 0; j < m; ++j) {       // 枚举位置i在时间集合j开启联合保护罩
                // 枚举 j 的补集 mask 中的子集 pre (即与j不重叠的所有其它时间集合pre)
                for (int mask = (m - 1) ^ j, pre = mask; ; pre = (pre - 1) & mask) {
                    int cost = dp[i - 1][pre] + union[j] + single[(mask ^ pre) & rain[i]];
                    dp[i][j] = Math.min(dp[i][j], cost);
                    if (pre == 0) break;        // 注意必须写在这里,不能在if里写pre != 0
                }
            }
        }
        return dp[n][0];
    }
}

1879. 两个数组最小的异或值之和

https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/

从集合论到位运算——常见位运算技巧及相关习题_第15张图片

class Solution {
    public int minimumXORSum(int[] nums1, int[] nums2) {
        int n = nums1.length;
        // dp[i]表示选择nums1中的集合i与nums2异或的最小和
        int[] dp = new int[1 << n];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 1; i < 1 << n; ++i) {
            int c = Integer.bitCount(i);
            for (int j = 0; j < n; ++j) {       // 枚举i的每一位
                if ((i >> j & 1) == 1) {
                    dp[i] = Math.min(dp[i], dp[i ^ (1 << j)] + (nums1[j] ^ nums2[c - 1]));
                }
            }
        }
        return dp[(1 << n) - 1];
    }
}

1986. 完成任务的最少工作时间段

https://leetcode.cn/problems/minimum-number-of-work-sessions-to-finish-the-tasks/
从集合论到位运算——常见位运算技巧及相关习题_第16张图片

类似题目 并行课程 II,相对更简单一些。

class Solution {
    public int minSessions(int[] tasks, int sessionTime) {
        int n = tasks.length;
        int[] dp = new int[1 << n], sum = new int[1 << n];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 0; i < 1 << n; ++i) {
            for (int j = 0; j < n; ++j) {
                sum[i] += (i >> j & 1) == 1? tasks[j]: 0;
            }
        }
        for (int i = 1; i < 1 << n; ++i) {                  // 枚举每种工作集合i
            for (int s = i; s != 0; s = (s - 1) & i) {      // 枚举i的每个子集s,作为这个工作时间段的工作
                if (sum[s] <= sessionTime) {
                    dp[i] = Math.min(dp[i], dp[i ^ s] + 1);
                }
            }
        }
        return dp[(1 << n) - 1];
    }
}

你可能感兴趣的:(算法,集合,位运算,动态规划,回溯)