BitWise-Operation

之前有个《位运算trick》的文章,也是讲位运算的。不过最近灵神发了个位运算的题单。这篇就按题单来了。

一、基础题

二、&/|

1. LC 2980 检查按位或是否存在尾随零

查是否能有至少两个偶数即可。

class Solution {
    public boolean hasTrailingZeros(int[] nums) {
        int cnt = 0;
        for (int num : nums) {
            if(num%2==0){
                cnt++;
                
                if(cnt==2){
                    return true;
                }
            }
        }
        
        return false;
    }
}

2. LC 1318 或运算的最小翻转次数

这题就给了1383分,但我写得巨抽象。

开了俩哈希表记录每个位置上的1的出现次数,如果a|b和c的对应哈希表中某个位置上的1的个数有且仅有一个为0,那么就查谁是0,a|b的是0那就改一次变成1就行;否则得改cnt1[i]次变成0。

class Solution {
    public int minFlips(int a, int b, int c) {
        int[] cnt1 = new int[32];
        int[] cnt2 = new int[32];

        updateCnt(a,cnt1);
        updateCnt(b,cnt1);
        updateCnt(c,cnt2);

        int ans = 0;
        for (int i = 0; i < 32; i++) {
            if((cnt1[i]*cnt2[i]==0)&&(cnt1[i]+cnt2[i]!=0)){
                if(cnt1[i]==0){
                    ans++;
                }else{
                    ans+=cnt1[i];
                }
            }
        }

        return ans;
    }

    private void updateCnt(int num, int[] cnt){
        int i = 0;
        while(num>0){
            cnt[i++]+=(num&1);
            num>>>=1;
        }
    }
}

看着写了一坨代码,实际上是32*4=128次常数次操作。O(1)的。

3. LC 2419 按位与最大的最长子数组

毁了,这题乱套公式常数时间垫底把自己道心干碎了。

总体来说&和|这俩都是越操作越xx的,比如&就是大,|就是小。所以照旧维护出现过的&值,并维护最小的左边界。遇到更大的&值就更新最大值和长度,遇到相同的就更新长度。

class Solution {
    public int longestSubarray(int[] nums) {
        int[][] and = new int[32][2];

        int len = 0;
        int max = 0;
        int ans = 1;
        for (int i = 0; i < nums.length; i++) {
            and[len][0] = Integer.MAX_VALUE;
            and[len++][1] = i;

            int j = 0;
            for (int k = 0; k < len; k++) {
                and[k][0] &= nums[i];

                if(and[j][0]!=and[k][0]){
                    and[++j][0] = and[k][0];
                    and[j][1] = and[k][1];
                }else{
                    and[j][1] = Math.min(and[j][1],and[k][1]);
                }

                if(max==and[j][0]){
                    ans = Math.max(ans,i-and[j][1]+1);
                }else if(max<and[j][0]){
                    max = and[j][0];
                    ans = 1;
                }
            }

            len = j+1;
        }

        return ans;
    }
}

但其实都看到越&越小的性质了,这个问题就变成了找最大值的连续串的最大长度的问题了。

class Solution {
    public int longestSubarray(int[] nums) {
        int max = 0;
        int ans = 0;
        int len = 0;

        for (int num : nums) {
            if(num>max){
                max = num;
                len = ans = 1;
            }else if(num==max){
                len++;
                ans = Math.max(ans,len);
            }else{
                len = 0;
            }
        }

        return ans;
    }
}

其实复杂度都是O(n),但后者常数好。

4. LC 2871 将数组分割成最多数目的子数组

先来说一个我的比较朴素的思路:

因为越&越小,所以我们先看一下整个数组&完的那个值是多少。假设&完为target,那么如果target不为0,说明答案肯定是1。因为这代表了任何一个子数组的分数不为0,所以但凡分割成超过一个子数组,那分数和一定到不了最小。

如果为0,就可以分组循环尝试把位与的值降到≤target。可能会担心说前面的都能降到≤target,后面的降不到怎么办?很简单,合并到前面的就可以了。

class Solution {
    public int maxSubarrays(int[] nums) {
        int target = Integer.MAX_VALUE;
        for (int num : nums) {
            target &= num;
        }

        if(target!=0){
            return 1;
        }

        int i = 0;
        int n = nums.length;
        int ans = 0;
        while(i<n){
            int tmp = Integer.MAX_VALUE;
            while(i<n&&tmp>target){
                tmp &= nums[i];
                i++;
            }
            ans++;
            if(i==n&&tmp>target){
                ans--;
            }
        }

        return ans;
    }
}

然后就有个优化的写法。你不是位与等于0才能下一段分组吗?我上来直接对全1串位与,如果遇到0给答案增一,不是0就直接一直往下跑,到最后返回答案就行,如果整个数组位与完都不是0,那么就返回1。

class Solution {
    public int maxSubarrays(int[] nums) {
        int a = -1;
        int ans = 0;
        for (int num : nums) {
            a &= num;
            if (a == 0) {
                ans++;
                a = -1;
            }
        }
        return Math.max(ans, 1);
    }
}

这里利用-1的补码是全1串,所以令a=-1。

这俩方法复杂度都是O(n),但是后面的常数时间好。前面的思路更加清晰。

5. LC 2401 最长优雅子数组

这题我的想法一开始是分组循环。这里对任意两个元素位与为0的优化是利用一个bitmap来存储各个位是否是1,这样新来的数是否和之前的任意一个数位与不为0等价于他和这个bitmap位与不为0。但是这个和分组循环不同的是,你nums[i:j]是一个组的话,不代表nums[i+1:j+x]不是一个合法的组。所以外层循环的时候要把i=j改成i++。

class Solution {
    public int longestNiceSubarray(int[] nums) {
        int ans = 1;
        int i = 0;
        int n = nums.length;

        while(i<n){
            int j = i+1;
            int bitmap = nums[i];
            while(j<n){
                if((bitmap&nums[j])!=0){
                    break;
                }else{
                    bitmap |= nums[j];
                }
                j++;
            }

            ans = Math.max(j-i,ans);
            i++;
        }

        return ans;
    }
}

这样最坏其实是O(n²)的,按1e5基本上是T了,但不知道这题为啥没卡。

比较好的写法是,不定长的滑动窗口,维护一个左边界一个右边界,如果bitmap和新来的位与不为0就增加左边界。然后增加右边界,这样滑窗就行。

可能会担心一个位上有多个数的1,这不用担心,因为每次我们都是确保了和新来的位与不为0才把新来的|到bitmap的,所以不可能有多个重复的1。

class Solution {
    public int longestNiceSubarray(int[] nums) {
        int ans = 0;
        for (int left = 0, right = 0, or = 0; right < nums.length; right++) {
            while ((or & nums[right]) > 0) // 有交集
                or ^= nums[left++]; // 从 or 中去掉集合 nums[left]
            or |= nums[right]; // 把集合 nums[right] 并入 or 中
            ans = Math.max(ans, right - left + 1);
        }
        return ans;
    }
}

6. LC 2680 最大或值

这个是个贪心,但也不太明显。把左移机会全给一个数才有可能达到或值最大。这样维护前后缀或值然后枚举左移k次的数就可以。

class Solution {
    public long maximumOr(int[] nums, int k) {
        int n = nums.length;
        if(n==1){
            return ((long)nums[0])<<k;
        }
        long[] prefix = new long[n];
        long[] suffix = new long[n];

        prefix[0] = nums[0];
        suffix[n-1] = nums[n-1];

        for (int i = 1; i < n; i++) {
            prefix[i] = prefix[i-1]|nums[i];
        }

        for (int i = n-2; i >= 0; i--) {
            suffix[i] = suffix[i+1]|nums[i];
        }

        long ans = Math.max(((long)nums[0]<<k)|suffix[1],((long)nums[n-1]<<k)|prefix[n-2]);
        for (int i = 1; i < n-1; i++) {
            ans = Math.max(ans, ((long) nums[i] <<k)|prefix[i-1]|suffix[i+1]);
        }

        return ans;
    }
}

假设存在序列I={i1,i2,i3,…,in},其中∑I=k,且ip≥0(1≤p≤n)。那么把k分散到这n个数上,一定比不上对n个数中最大的左移k次或完的值要大,因为从0-1串的长度上来说已经输了。

这题有人用DP做的,他定义f[i][j]表示nums[0:i]个数用掉j次左移能得到的最大值。那么f[n-1][k]就是答案,状态转移方程为:

import static java.lang.Math.max;

class Solution {
    public long maximumOr(int[] nums, int k) {
        int n = nums.length;
        if(n==1){
            return ((long)nums[0])<<k;
        }

        long[][] f = new long[n][k + 1];

        for (int i = 0; i <= k; i++) {
            f[0][i] = ((long) nums[0]) << i;
        }

				// 状态转移方程
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= k; j++) {
                for (int z = 0; z <= j; z++) {
                    f[i][j] = max(f[i][j], f[i - 1][z] | (((long) nums[i]) << (j - z)));
                }
            }
        }

        return f[n-1][k];
    }
}

这个做法假了。例如:

[10,8,4]

按他的做法来说,前两个数用掉0次左移可以最大得到10,用掉1次左移可以最大得到28(20|8),但是实际上的最大是(10|16|4),而不是(20|8|4)。这个状态转移是错的。基于当前的最大不一定能得到下一次的最大。

7. LC 2411 按位或最大的最小子数组长度

跟LC 3097差不多。不过我这个选择的是倒着遍历的。因为他要求最大么,正着遍历的话不知道后面是否会出现更大的,但是倒着遍历就把后面元素全记录了,这样就是O(n)了。

大致思路如下:

  1. 倒着遍历元素,记录每一个或值及能够达成这个或值的最小右边界
  2. 把当前元素和记录的或值全部或起来,原地去重
  3. 或的同时记录最大值,查询这个最大值对应的右边界r,r-i+1即为ans[i]
class Solution {
    public int[] smallestSubarrays(int[] nums) {
        int n = nums.length;

        int[][] ors = new int[32][2];
        int m = 0;
        int[] ans = new int[n];

        for (int i = n-1; i >= 0; i--) {
            ors[m][0] = 0;
            ors[m++][1] = i;

            int j = 0;
            int max = 0;
            for (int k = 0; k < m; k++) {
                ors[k][0] |= nums[i];

                if(ors[k][0]!=ors[j][0]){
                    ors[++j][0] = ors[k][0];
                }

                ors[j][1] = ors[k][1];
                if(ors[max][0]<ors[j][0]){
                    max = j;
                }
            }

            m = j+1;

            ans[i] = ors[max][1]-i+1;
        }

        return ans;
    }
}

但这题还有个做法,就是正着遍历的过程中倒着遍历。可以这么想,如果正着遍历到一个新元素,导致之前以某个位置开始的或值变得更大,就可以更新这个位置的长度。如果不会造成更大,那么更之前的也不会更大(因为卡在了当前这里),再之前的也统统不用更新。

class Solution {
    public int[] smallestSubarrays(int[] nums) {
        int[] ans = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            ans[i] = 1;
            for (int j = i - 1; j >= 0 && (nums[j] | nums[i]) != nums[j]; j--) {
                nums[j] |= nums[i];
                ans[j] = i - j + 1;
            }
        }
        return ans;
    }
}

相关推荐

  1. BitWise-Operation

    2024-04-06 10:54:01       36 阅读
  2. Leetcode 3171. Find Subarray With Bitwise AND Closest to K

    2024-04-06 10:54:01       28 阅读
  3. operator <=> (spaceship operator)

    2024-04-06 10:54:01       35 阅读
  4. 【C++】operator()

    2024-04-06 10:54:01       63 阅读
  5. Cyclic Operations

    2024-04-06 10:54:01       24 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-06 10:54:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-06 10:54:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-06 10:54:01       82 阅读
  4. Python语言-面向对象

    2024-04-06 10:54:01       91 阅读

热门阅读

  1. rknn3588 yolov5 学习笔记

    2024-04-06 10:54:01       37 阅读
  2. [C++][特殊类设计][单例模式]详细讲解

    2024-04-06 10:54:01       31 阅读
  3. 2024.3.25力扣每日一题——零钱兑换2

    2024-04-06 10:54:01       39 阅读
  4. 数据大屏:现代数据分析与可视化的重要工具

    2024-04-06 10:54:01       35 阅读
  5. 蓝桥杯算法题:外卖店优先级

    2024-04-06 10:54:01       28 阅读
  6. C. Rudolf and the Ugly String

    2024-04-06 10:54:01       33 阅读
  7. 各种负载均衡技术

    2024-04-06 10:54:01       36 阅读
  8. Web 安全之 SSL 剥离攻击详解

    2024-04-06 10:54:01       30 阅读
  9. 【QT教程】QT6 QML在工业控制系统中的应用

    2024-04-06 10:54:01       30 阅读
  10. 30天拿下Rust之超级好用的“语法糖”

    2024-04-06 10:54:01       39 阅读
  11. 探索 AWK:Linux 下的文本处理

    2024-04-06 10:54:01       34 阅读