11### 题目描述
22
3- 这是 LeetCode 上的 ** [ 2044. 统计按位或能得到最大值的子集数目] ( ) ** ,难度为 ** 中等** 。
3+ 这是 LeetCode 上的 ** [ 2044. 统计按位或能得到最大值的子集数目] ( https://leetcode-cn.com/problems/count-number-of-maximum-bitwise-or-subsets/solution/by-ac_oier-dos6/ ) ** ,难度为 ** 中等** 。
44
5- Tag : 「二进制枚举」、「位运算」、「回溯算法 」
5+ Tag : 「二进制枚举」、「位运算」、「DFS」、「状压 DP 」
66
77
88
9- 给你一个整数数组 $nums$ ,请你找出 $nums$ 子集 ** 按位或** 可能得到的 最大值 ,并返回按位或能得到最大值的 ** 不同非空子集的数目** 。
9+ 给你一个整数数组 $nums$ ,请你找出 $nums$ 子集 ** 按位或** 可能得到的 ** 最大值** ,并返回按位或能得到最大值的 ** 不同非空子集的数目** 。
1010
1111如果数组 $a$ 可以由数组 $b$ 删除一些元素(或不删除)得到,则认为数组 $a$ 是数组 $b$ 的一个 子集 。如果选中的元素下标位置不一样,则认为两个子集 不同 。
1212
13- 对数组 $a$ 执行 按位或 ,结果等于 $a[ 0] $ ` OR ` $a[ 1] $ ` OR ` ` ... ` ` OR ` $a[ a.length - 1] $(下标从 0ドル$ 开始)。
13+ 对数组 $a$ 执行 ** 按位或** ,结果等于 $a[ 0] $ ` OR ` $a[ 1] $ ` OR ` ` ... ` ` OR ` $a[ a.length - 1] $(下标从 0ドル$ 开始)。
1414
1515示例 1:
1616```
@@ -60,7 +60,7 @@ Tag : 「二进制枚举」、「位运算」、「回溯算法」
6060在枚举这 2ドル^n$ 个状态过程中,我们使用变量 ` max ` 记录最大的按位或得分,使用 ` ans ` 记录能够取得最大得分的状态数量。
6161
6262代码:
63- ``` Java
63+ ``` Java
6464class Solution {
6565 public int countMaxOrSubsets (int [] nums ) {
6666 int n = nums. length, mask = 1 << n;
@@ -80,17 +80,68 @@ class Solution {
8080 }
8181}
8282```
83- * 时间复杂度:令 $nums$ 长度为 $n,ドル共有 2ドル^n$ 个子集状态,计算每个状态的按位或答案复杂度为 $O(n)。$ 整体复杂度为 $O(2^n * n)$
83+ * 时间复杂度:令 $nums$ 长度为 $n,ドル共有 2ドル^n$ 个子集状态,计算每个状态的按位或得分的复杂度为 $O(n)$。 整体复杂度为 $O(2^n * n)$
8484* 空间复杂度:$O(1)$
8585
86+ 8687---
8788
88- ### 回溯算法
89+ ### 状压 DP
90+ 91+ 为了优化解法一中「每次都要计算某个子集的得分」这一操作,我们可以将所有状态的得分记下来,采用「动态规划」思想进行优化。
92+ 93+ 需要找到当前状态 $state$ 可由哪些状态转移而来:假设当前 $state$ 中处于最低位的 1ドル$ 位于第 $idx$ 位,首先我们可以使用 ` lowbit ` 操作得到「仅保留第 $idx$ 的 1ドル$ 所对应的数值」,记为 $lowbit,ドル那么显然对应的状态方程为:
94+ 95+ $$
96+ f[state] = f[state - lowbit] \wedge nums[idx]
97+ $$
98+ 99+ 再配合我们从小到大枚举所有的 $state$ 即可确保计算 $f[ state] $ 时所依赖的 $f[ state - lowbit] $ 已被计算。
100+ 101+ 最后为了快速知道数值 $lowbit$ 最低位 1ドル$ 所处于第几位(也就是 $idx$ 为何值),我们可以利用 $nums$ 长度最多不超过 16ドル$ 来进行「打表」预处理。
102+ 103+ 代码:
104+ ``` Java
105+ class Solution {
106+ static Map<Integer , Integer > map = new HashMap<> ();
107+ static {
108+ for (int i = 0 ; i < 20 ; i++ ) map. put((1 << i), i);
109+ }
110+ public int countMaxOrSubsets (int [] nums ) {
111+ int n = nums. length, mask = 1 << n;
112+ int [] f = new int [mask];
113+ int max = 0 , ans = 0 ;
114+ for (int s = 1 ; s < mask; s++ ) {
115+ int lowbit = (s & - s);
116+ int prev = s - lowbit, idx = map. get(lowbit);
117+ f[s] = f[prev] | nums[idx];
118+ if (f[s] > max) {
119+ max = f[s]; ans = 1 ;
120+ } else if (f[s] == max) {
121+ ans++ ;
122+ }
123+ }
124+ return ans;
125+ }
126+ }
127+ ```
128+ * 时间复杂度:$O(2^n)$
129+ * 空间复杂度:$O(2^n)$
130+ 131+ ---
132+ 133+ ### DFS
134+ 135+ 解法一将「枚举子集/状态」&「计算状态对应的得分」两个过程分开进行,导致了复杂度上界为 $O(2^n * n)$。
136+ 137+ 事实上,我们可以在「枚举子集」的同时「计算相应得分」,设计 ` void dfs(int u, int val) ` 的 ` DFS ` 函数来实现「爆搜」,其中 $u$ 为当前的搜索到 $nums$ 的第几位,$val$ 为当前的得分情况。
89138
139+ 对于任意一位 $x$ 而言,都有「选」和「不选」两种选择,分别对应了 ` dfs(u + 1, val | nums[x]) ` 和 ` dfs(u + 1, val) ` 两条搜索路径,在搜索所有状态过程中,使用全局变量 ` max ` 和 ` ans ` 来记录「最大得分」以及「取得最大得分的状态数量」。
90140
141+ 该做法将多条「具有相同前缀」的搜索路径的公共计算部分进行了复用,从而将算法复杂度下降为 $O(2^n)$。
91142
92143代码:
93- ``` Java
144+ ``` Java
94145class Solution {
95146 int [] nums;
96147 int max = 0 , ans = 0 ;
@@ -113,7 +164,7 @@ class Solution {
113164 }
114165}
115166```
116- * 时间复杂度:令 $nums$ 长度为 $n,ドル共有 2ドル^n$ 个子集状态。$ 整体复杂度为 $O(2^n * n)$
167+ * 时间复杂度:令 $nums$ 长度为 $n,ドル共有 2ドル^n$ 个子集状态。整体复杂度为 $O(2^n)$
117168* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$
118169
119170---
0 commit comments