本文的内容主要是 马住 一些 关于 位运算的操作。
以及下面的练习题比较有质量。
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 元子集) 可见: 位运算技巧
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;
}
}
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;
}
}
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);
}
}
}
}
https://leetcode.cn/problems/maximum-and-sum-of-array/
提示:
n == nums.length
1 <= numSlots <= 9
1 <= n <= 2 * numSlots
1 <= nums[i] <= 15
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;
}
}
参见:【LeetCode周赛】2022上半年题目精选集——动态规划
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:我在官解的评论区写了自己错误的代码,等待有人解答哪里有错误。
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];
}
}
这道题目需要学会 枚举一个集合的所有子集 的方法。
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];
}
}
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];
}
}
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];
}
}
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];
}
}