LeetCode 2044. 统计按位或能得到最大值的子集数目(位运算+回溯/状压DP)

本文介绍了三种方法来解决统计数组子集中按位或操作能获得最大值的子集数目问题。方法一通过二进制枚举,方法三利用状态压缩动态规划,方法二采用回溯搜索。每种方法的时间复杂度和空间复杂度都进行了分析,并给出了详细的代码实现。这些方法展示了如何在信息技术领域中高效地处理组合优化问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


题目描述

在这里插入图片描述

在这里插入图片描述

方法一:二进制枚举

n 是数组 nums \textit{nums} nums 的长度,数组中的每个元素都可以选取或者不选取,因此数组的非空子集数目一共有 ( 2 n − 1 ) (2^n-1) (2n1)个。

用一个长度为 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[ilowbit]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) (2n1)个。

可以在「枚举子集」的同时「计算相应得分」,设计 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); 两条搜索路径,在搜索所有状态过程中,使用全局变量 maxcnt 来记录「最大得分」以及「取得最大得分的状态数量」。

该做法将多条「具有相同前缀」的搜索路径的公共计算部分进行了复用,从而将算法复杂度下降为 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

Reference

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xylitolz

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

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

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

打赏作者

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

抵扣说明:

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

余额充值