Leet code 982.按位与为零的三元组

文章介绍了三种解决数组中寻找三元组与运算为零的方法,包括原始的三重循环法、使用哈希表优化的算法以及进一步的子集优化算法,详细分析了每种方法的时间复杂度和空间复杂度。

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

题目重述

示例 

法一:三重循环

        最容易想到的方法应该是直接三重循环 这里有一个小技巧,就是并不用对i,j,k都进行0-length的搜索,而是让i<=j<=k; 

        之后根据重复次数在进行叠加即可,这种方法可以通过java的评测,但不能通过c++的评测

        int count=0;
        for(int i=0;i<nums.length;i++){
            for(int j=i;j<nums.length;j++){
                for(int k=j;k<nums.length;k++){
                    if((nums[i]&nums[j]&nums[k])==0){
                        if(i==j&&j==k){
                            count++;
                        }else if(i==j||j==k){ //不用写i==k, 因为i==k的时候其实是i,j,k全相等的
                            count+=3;
                        }else {
                            count +=6;
                        }
                    }
                }
            }
        }
        return count;

        时间复杂度O(n^3);

        空间复杂度O(1);

 法二:加入hash优化算法

        在思考完暴力解法后,我们开始尝试优化算法.

        考虑到题目的要求是三元组的与&结果为0,而对于三元组很容易想到的一个优化是先做二元组,此时我们把二元组用hash存储以降低时间复杂度,注意到数据范围在 [0,2^{16}),那么显然nums[i]&nums[j]也处于这个范围[0,2^{16});

        我们先使用二重循环枚举i,j并用一个长度为2^{16}的vector记录出现的每一种nums[i]&nums[j]的出现次数,未出现的默认0次,之后我们再使用二重循环,其中一重为k的枚举,另一重为记录出现频数的vector数组的枚举.+=即可(默认为0)

        代码如下:(注意==优先级大于&)

        int count=0;
        vector<int> cnt(1<<16,0);//初始化一个2^16的数组  
        for(int i=0;i<nums.size();i++)
            for(int j=0;j<nums.size();j++)
                cnt[nums[i]&nums[j]]++;

        for(int k=0;k<nums.size();k++){
            for(int x=0;x< (1<<16);x++){
                if((nums[k]&x)==0) 
       //注意这里要有一个()    
       //对于表达式a&b==0,我们以为应该先判断a&b,再判断是否等于0,其实恰好相反
                    count+=cnt[x];
            }
        }
        return count;

       时间复杂度O(n^2+Cn);

        空间复杂度O(C);  

        C=2^{16} 也就是vector的size    

法三:hash + 子集优化

        子集优化的主要目标是减小法二中的C

        子集优化的主要做法是不再以 if((nums[k]&x)==0)  去匹配 0~2^{16} 的每一个可能的(nums[i]&nums[j]),而是针对于nums[k]去统计每一个与它自身可以相与&为0的二元组nums[i]&nums[j]的总数.

        这里我们把nums[k]与2^{16}-1进行异或运算得到y,这样一来,满足要求的二元组的二进制表示中所包含的1就一定需要是y的子集.

        举个例子,比如nums[k]是011000,(我们只考虑6位,其余同理),则y就是100111,那么与nums[k]相与&为0的就一共有16种,{每一种的1的位置} \epsilon {y的1的位置} ,是它的子集. 

         那么我们怎么枚举子集呢,这里运用了二进制枚举子集的技巧:

 记当前异或后的二进制数是y,我们要求y的所有 (1子集)

一:
    首先令sub=y因为它本身也是自己的子集,
二:
    接着让sub = (sub-1) & y;直到sub=0;


//简单理解:
    sub从y走到0,每次-1而不是-2,因此可以走到所有可能位置,而与y进行与操作,又保证它一定是y的(1子集).

//深层理解:
    以y=10100为例,二进制下减1->10011
        即把最右的1变0,把该1后面的所有0变1
  
    而这个时候再对y进行与操作,相当于总体上是
        即把最右的1变0,把该1后面的所有y上面1的位置变1,其余保持0不变
        相当于二进制的跨步减法. 10100->10000->00100->00000.

//还是无法理解的建议算一下这个例子

        代码如下:


        vector<int> cnt(1<<16,0);
        for(int x:nums)
            for(int y:nums)
                cnt[x&y]++;

        int count=0;
        for(int x:nums){
            // 找到所有与nums[k]相&得 0的二元组(不计顺序)个数
            int y=x^0xffff; //得到x与2^16的 异或
            // 找到此时y的所有1子集
            for(int sub=y;sub!=0;sub=(sub-1)&y){
                count+=cnt[sub];
            } 
            count +=cnt[0];
        }
        return count;

       时间复杂度O(n^2+Cn);

        空间复杂度O(C);  

        对于C,当数据x随机时,x(nums[k])的二进制形式中1的个数期望为8个,二进制枚举1子集需要枚举2^{8}次, 当最坏情况下,x的二进制形式中1个数为16个,二进制枚举需要枚举2^{16}次. 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值