题目描述
方法一:二进制枚举
记 n
是数组
nums
\textit{nums}
nums 的长度,数组中的每个元素都可以选取或者不选取,因此数组的非空子集数目一共有
(
2
n
−
1
)
(2^n-1)
(2n−1)个。
用一个长度为 n n n 比特的整数来表示不同的子集,在整数的二进制表示中, n n n 个比特的值代表了对数组不同元素的取舍:
- 第 i i i 位值为 1 1 1 则表示该子集选取对应元素
- 第 i i i 位值为 0 0 0 则表示该子集不选取对应元素
在枚举这
2
n
2^n
2n 个状态过程中,使用变量 maxOr
记录最大的按位或得分,使用 cnt
记录能够取得最大得分的状态数量。
class Solution {
public int countMaxOrSubsets(int[] nums) {
int maxOr = 0, cnt = 0;
// i : 不同的子集
for (int i = 0; i < 1 << nums.length; i++) {
int orVal = 0;
for (int j = 0; j < nums.length; j++) {
if (((i >> j) & 1) == 1) {
// 求每个子集按位或的值
orVal |= nums[j];
}
}
if (orVal > maxOr) {
maxOr = orVal;
cnt = 1;
} else if (orVal == maxOr) {
cnt++;
}
}
return cnt;
}
}
-
时间复杂度: O ( 2 n × n ) O(2^n \times n) O(2n×n),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要遍历 O ( 2 n ) O(2^n) O(2n) 个状态,遍历每个状态时需要遍历 O ( n ) O(n) O(n) 位。
-
空间复杂度: O ( 1 ) O(1) O(1)。仅使用常量空间。
方法三:状态压缩DP
为了优化方法一中「遍历每个状态时需要遍历 O ( n ) O(n) O(n) 位」这一操作,可以将所有状态的得分记下来,采用「动态规划」思想进行优化。
需要找出状态转移方程:
- 假设当前状态
i
中处于最低位的1
位于第idx
位(即该子集选取了nums[idx]
),可以利用x & -x
的操作得到「仅保留第 i d x idx idx 位的1
所对应的数值(该状态表示子集中仅有nums[idx]
这一个元素)」,记为lowbit
,因此状态转移方程为:
f [ i ] = f [ i − l o w b i t ] ∣ n u m s [ i d x ] f[i] = f[i - lowbit]\, | \,nums[idx] f[i]=f[i−lowbit]∣nums[idx]
该转态转移方程可翻译为:状态i
所表示的子集方案可由不包含nums[idx]
的子集方案i-lowbit
加上nums[idx]
而来。
从小到大枚举所有的 状态i
即可确保计算 f[i]
时所依赖的 f[i - lowbit]
已被计算。
最后为了快速知道数值 lowbit
最低位 1
所处于第几位(也就是 idx
为何值),可以利用 nums
长度最多不超过 16 来进行「打表」预处理。
class Solution {
public int countMaxOrSubsets(int[] nums) {
// 打表
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i <= 16; i++) {
map.put((1 << i), i);
}
int n = nums.length, mask = 1 << n;
int[] f = new int[mask];
int max = 0, cnt = 0;
// 状态 i
for (int i = 1; i < mask; i++) {
int lowbit = (i & -i);
int prev = i - lowbit, idx = map.get(lowbit);
f[i] = f[prev] | nums[idx];
if (f[i] > max) {
max = f[i];
cnt = 1;
} else if (f[i] == max) {
cnt++;
}
}
return cnt;
}
}
- 时间复杂度: O ( 2 n ) O(2^n) O(2n)
- 空间复杂度: O ( 2 n ) O(2^n) O(2n)
方法二:回溯
记 n
是数组
nums
\textit{nums}
nums 的长度,数组中的每个元素都可以选取或者不选取,因此数组的非空子集数目一共有
(
2
n
−
1
)
(2^n-1)
(2n−1)个。
可以在「枚举子集」的同时「计算相应得分」,设计 void dfs(int[] nums, int i, int res)
的 DFS
函数来实现「爆搜」,其中 i
为当前的搜索到 nums
的第几位,res
为当前的得分情况。
对于任意一位 nums[i]
而言,都有「选」和「不选」两种选择( LeetCode 78. 子集),分别对应了 dfs(nums, i + 1, res | nums[i]);
和 dfs(nums, i + 1, res);
两条搜索路径,在搜索所有状态过程中,使用全局变量 max
和 cnt
来记录「最大得分」以及「取得最大得分的状态数量」。
该做法将多条「具有相同前缀」的搜索路径的公共计算部分进行了复用,从而将算法复杂度下降为 O ( 2 n ) O(2^n) O(2n)。
class Solution {
int max = 0;
int cnt = 0;
public int countMaxOrSubsets(int[] nums) {
dfs(nums, 0, 0);
return cnt;
}
private void dfs(int[] nums, int i, int res) {
if (i == nums.length) {
if (res > max) {
max = res;
cnt = 1;
} else if (res == max) {
cnt++;
}
return ;
}
// 选取
dfs(nums, i + 1, res | nums[i]);
// 不选
dfs(nums, i + 1, res);
}
}
-
时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n n n 是数组 nums \textit{nums} nums 的长度。状态数一共有 O ( 2 0 + 2 1 + . . . + 2 n ) = O ( 2 × 2 n ) = O ( 2 n ) O(2^0 + 2^1 + ... + 2^n) = O(2\times2^n) = O(2^n) O(20+21+...+2n)=O(2×2n)=O(2n) 种,每次计算只消耗常数时间。
-
空间复杂度: O ( n ) O(n) O(n),其中 nn 是数组 nums \textit{nums} nums 的长度。搜索深度最多为 n n n。