Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
### 题目描述

这是 LeetCode 上的 **[2044. 统计按位或能得到最大值的子集数目]()** ,难度为 **中等**。

Tag : 「二进制枚举」、「位运算」、「回溯算法」



给你一个整数数组 $nums$ ,请你找出 $nums$ 子集 **按位或** 可能得到的 最大值 ,并返回按位或能得到最大值的 **不同非空子集的数目** 。

如果数组 $a$ 可以由数组 $b$ 删除一些元素(或不删除)得到,则认为数组 $a$ 是数组 $b$ 的一个 子集 。如果选中的元素下标位置不一样,则认为两个子集 不同 。

对数组 $a$ 执行 按位或 ,结果等于 $a[0]$ `OR` $a[1]$ `OR` `...` `OR` $a[a.length - 1]$(下标从 $0$ 开始)。

示例 1:
```
输入:nums = [3,1]

输出:2

解释:子集按位或能得到的最大值是 3 。有 2 个子集按位或可以得到 3 :
- [3]
- [3,1]
```
示例 2:
```
输入:nums = [2,2,2]

输出:7

解释:[2,2,2] 的所有非空子集的按位或都可以得到 2 。总共有 23 - 1 = 7 个子集。
```
示例 3:
```
输入:nums = [3,2,1,5]

输出:6

解释:子集按位或可能的最大值是 7 。有 6 个子集按位或可以得到 7 :
- [3,5]
- [3,1,5]
- [3,2,5]
- [3,2,1,5]
- [2,5]
- [2,1,5]
```

提示:
* $1 <= nums.length <= 16$
* $1 <= nums[i] <= 10^5$

---

### 二进制枚举

令 $n$ 为 $nums$ 的长度,利用 $n$ 不超过 $16$,我们可以使用一个 `int` 数值来代指 $nums$ 的使用情况(子集状态)。

假设当前子集状态为 $state$,$state$ 为一个仅考虑低 $n$ 位的二进制数,当第 $k$ 位为 $1$,代表 $nums[k]$ 参与到当前的按位或运算,当第 $k$ 位为 $0$,代表 $nums[i]$ 不参与到当前的按位或运算。

在枚举这 $2^n$ 个状态过程中,我们使用变量 `max` 记录最大的按位或得分,使用 `ans` 记录能够取得最大得分的状态数量。

代码:
```Java
class Solution {
public int countMaxOrSubsets(int[] nums) {
int n = nums.length, mask = 1 << n;
int max = 0, ans = 0;
for (int s = 0; s < mask; s++) {
int cur = 0;
for (int i = 0; i < n; i++) {
if (((s >> i) & 1) == 1) cur |= nums[i];
}
if (cur > max) {
max = cur; ans = 1;
} else if (cur == max) {
ans++;
}
}
return ans;
}
}
```
* 时间复杂度:令 $nums$ 长度为 $n$,共有 $2^n$ 个子集状态,计算每个状态的按位或答案复杂度为 $O(n)。$整体复杂度为 $O(2^n * n)$
* 空间复杂度:$O(1)$

---

### 回溯算法



代码:
```Java
class Solution {
int[] nums;
int max = 0, ans = 0;
public int countMaxOrSubsets(int[] _nums) {
nums = _nums;
dfs(0, 0);
return ans;
}
void dfs(int u, int val) {
if (u == nums.length) {
if (val > max) {
max = val; ans = 1;
} else if (val == max) {
ans++;
}
return ;
}
dfs(u + 1, val);
dfs(u + 1, val | nums[u]);
}
}
```
* 时间复杂度:令 $nums$ 长度为 $n$,共有 $2^n$ 个子集状态。$整体复杂度为 $O(2^n * n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2044` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,32 @@ class Solution {

---

### 答疑

评论区有位同学提出了一个挺有意思的疑问,或许是部分同学的共同疑问,这里集中答疑一下。

Q0: `for` 循环里的 `ans.clear()` 这个函数也是 $O(n)$ 复杂度吧,为什么合起来还是 $O(n)$ ?

A0: 在 `ArrayList` 源码中的 `clear` 实现会为了消除容器对对象的强引用,遍历容器内的内容并置空来帮助 GC。

但不代表这会导致复杂度上界变成 $n^2$。

不会导致复杂度退化的核心原因是:**由于 `clear` 导致的循环计算量总共必然不会超过 $n$**。因为最多只有 $n$ 个元素在 `ans` 里面,且同一元素不会被删除多次(即每个元素对 `clear` 的贡献不会超过 $1$)。

如果有同学还是觉得不好理解,可以考虑一种极端情况:`clear` 操作共发生 $n$ 次,但发生 $n$ 次的前提条件是每次 `ans` 中只有 $1$ 位元素,此时由 `clear` 操作带来的额外计算量为最大值 $n$。

因此这里的 `clear` 操作对复杂度影响是「累加」,而不是「累乘」,即复杂度仍为 $O(n)$,而不是 $O(n^2)$。

<br/>

Q1: 判断 $list[i]$ 是否在哈希中的操作,复杂度是多少?

A1: 在 Java 的 `HashMap` 实现中,当键值对中的键数据类型为 `String` 时,会先计算一次(之后使用缓存)该字符串的 `HashCode`,计算 `HashCode` 的过程需要遍历字符串,因此该操作是与字符串长度相关的(对于本题字符串长度不超过 $30$),然后根据 `HashCode`「近似」$O(1)$ 定位到哈希桶位置并进行插入/更新。

因此在 Java 中,该操作与「当前的字符串长度」相关,而与「当前容器所包含元素多少」无关。

---

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.599` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
Expand Down