从集合论到位运算——常见位运算技巧及相关习题 & 状态压缩DP

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

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

原文链接

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

在这里插入图片描述

集合与集合

在这里插入图片描述

集合与元素

在这里插入图片描述
在这里插入图片描述

遍历集合

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

枚举集合

在这里插入图片描述
在这里插入图片描述
关于 Gosper’s Hack (生成 n元集合所有 k 元子集) 可见: 位运算技巧


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

在这里插入图片描述


题目练习

位运算

78. 子集

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

在这里插入图片描述

可以用 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/

在这里插入图片描述

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/

在这里插入图片描述

用 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/
在这里插入图片描述

提示:
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/
在这里插入图片描述

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/
在这里插入图片描述

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/
在这里插入图片描述

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

如果可以学习的课程集合 <= 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/

在这里插入图片描述

提示:
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/

在这里插入图片描述

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/
在这里插入图片描述

类似题目 并行课程 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];
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
要证明0-1整数规划问题是NPC问题,可以采用约简证明法,即将已知的NPC问题归约为0-1整数规划问题,证明该问题也是NPC问题。 首先,我们知道背包问题是NPC问题,即在有限的背包容量下,选取一些物品使得总价值最大。将背包问题约化为0-1整数规划问题如下: 设物品集合为S={1,2,...,n},物品i的重量为wi,价值为vi,背包容量为W。引入0-1变量xi表示是否选择物品i,即xi=0或1。则0-1整数规划问题可以表示为: max Σvi * xi s.t. Σwi * xi <= W xi ∈ {0,1} 可以看出,0-1整数规划问题是背包问题的一种特殊形式,因此0-1整数规划问题也是NPC问题。 然后,我们可以将背包问题约化为0-1整数规划问题的过程中,使用动态规划方法来解决问题。具体来说,可以使用状态压缩DP来解决0-1整数规划问题。状态压缩DP是一种将集合压缩为二进制数表示的动态规划方法,可以用来解决一些具有集合结构的问题,如背包问题、旅行商问题等。 在0-1整数规划问题中,可以使用状态压缩DP来表示选取物品的情况。假设当前背包容量为j,已经考虑了前i个物品,则可以使用一个二进制数表示当前选取的物品集合,即将第k位设为1表示选取了第k个物品,为0表示没有选取。则状态转移方程为: dp[i][j][S] = max(dp[i-1][j][S], dp[i-1][j-wi][S-vi] + vi) 其中,dp[i][j][S]表示考虑前i个物品,背包容量为j,集合状态为S时的最大价值;wi和vi分别表示第i个物品的重量和价值;S-vi表示将第i个物品加入集合S后的新集合状态。 通过状态压缩DP的方法,可以在多项式时间内解决0-1整数规划问题,进一步证明了该问题是NPC问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wei *

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值