位运算trick

位运算本质上不是一种算法,而是一种trick,用来节约时间/空间的trick。背后常常有集合论、状态压缩等思想的支撑。这里探讨的位运算指的是其背后的指导思想而不是trick本身。因此对trick本身的证明就略过了。

位运算各种trick的详解请参照灵神的教学贴:leetcode.cn/circle/discuss/CaOJ45/

如果想获取位运算的知识图谱,以及集合论的一些基础知识。我在子集状压DP篇收录了相关图片(搬运别人的),可以在该博客找到。

一、集合论

常见的有枚举已知集合的子集、判断子集元素、集合交并补(对称)差等。

1.1. LC 78 子集

这里涉及最简单的枚举已知集合的子集。我们可以为集合中的每个元素编号,假设集合的势为n,则编号为0~(n-1),那么子集就可以写作一个0-1串,选取则为1,不选则为0。采用小端法。

例如,共计4个元素,选取第0个和第2个,就是0101。我们把这个0-1串看成二进制,则它在十进制下就是5。而这种映射显然是一一对应的,双射。因此5就代表了子集{0,2},用一个4字节整型就压缩了一个子集。

现在既然要枚举子集,那么就要确定子集的范围,很明显是全零到全一,也就是[ 0 , 2^n-1 ]。对于每个数我们判断子集元素即可。

那么如何判断子集元素?每次位于1,若为1则说明末位为1,应选取,然后整个数右移更新末位,循环直至整个0-1串被遍历即可。(或者可以把1移位,这个看个人喜好)

另外这一题是一道比较经典的题,他还能训练深搜回溯的基础算法。这里强调位运算集合论,就不放了。

复杂度O(n*2^n):共O(2^n)个子集,每个移位判断O(n)次。

import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        // 位运算写法枚举子集
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        // [ 0 , 1 << n - 1 ] 不选对应0
        int j = 1 << n;
        for (int i = 0; i < j; i++) {
            ArrayList<Integer> sub = new ArrayList<>();
            int tmp = i;
            int index = 0;
            while(tmp>0){
                if((tmp&1)==1){
                    sub.add(nums[index]);
                }
                index++;
                tmp >>= 1;
            }
            ans.add(sub);
        }
        return ans;
    }
}

2.1. LC 2732 找到矩阵中的好子集

这题思路比较明确(我看提示了(笑哭)),就是任何一个合法子集中每一列至少有一个0。(如果全1那么和行数一样了,肯定没一半大)这样我们就能把子集压缩到最大行数为2的情况。

  1. 如果某一行全是0,直接返回这一行索引
  2. 如果两行&完是0,返回这两行索引

我的实现是:

  1. 先对每行位运算状压,作为一个int放到列表

  2. 对于每一个int a,生成一个列表[int],这个列表中的所有int b满足

    1. b的有效位长度和a一致,也即矩阵列数
    2. a&b = 0

    例如 a = binary(1101) 则合法的b有 [binary(0010),binary(0000)] 可以发现,由于n<=5,因此至多生成2^n=32个b

  3. 开一个hash表维护每个int a对应的索引,遍历之前生成的列表,查找这个列表中的值对应的索引j,如果有,返回[i,j]

from typing import List

class Solution:
    def goodSubsetofBinaryMatrix(self, grid: List[List[int]]) -> List[int]:
        arr = []

        def map_binary(row:List[int])->int:
            res = 0
            for i,x in enumerate(row):
                res |= (x<<i)
            return res
        
        def match_generate(row:int)->List[int]:
            res = []
            def dfs(row:int,index:int,prefix:int):
                if index >= len(grid[0]):
                    res.append(prefix)
                    return
                dfs(row,index+1,prefix|(0<<index))
                if (row>>index)&1==0:
                    dfs(row,index+1,prefix|(1<<index))
            dfs(row,0,0)
            return res
                    
        
        for row in grid:
            arr.append(map_binary(row))
        
        n = len(arr)
        rec = {}
        for i in range(n):
            if arr[i]==0:
                return [i]
            match = match_generate(arr[i])
            # print(match)
            for res in match:
                if res in rec:
                    return [rec[res],i]
            rec[arr[i]] = i
        
        return []

这题我有个运行时间上的问题,详情见于:每日一题测评运行时间问题:LC 2732 找到矩阵中的好子集 - 力扣(LeetCode)

二、 位计数

2.1. LC 2997 使数组异或和等于K的最少操作次数

这题一看提交里的解答,我直接小脑萎缩。

class Solution {
    public int minOperations(int[] nums, int k) {
        for(int x:nums){
            k^=x;
        }
        return Integer.bitCount(k);
    }
}

我写了60+行,人家4行结束了。意思也很明了,先把已有的异或匹配,然后数剩下多少1就完事了。我写的那60+行就是模拟了这个过程。

2.2. LC 3097 或值至少为K的最短子数组Ⅱ

举个例子:

nums = [ 1,2,3,4,5 ]

假设i是我们遍历数组时的索引

  1. i=0 只有一个或值:1
  2. i=1 两个或值:3,2(1和新来的2或,得到3;2自己或,得到2)
  3. i=2 一个或值:3,3,3(3和新来的3或,得到3;2和新来的3或,得到2;3自己或,得到3)
  4. i=3 两个或值:7,4(3和4;4自己)

那么此时想要得到7的话,最短的子数组就是[3,4]。那么怎么知道从哪里开始最短呢?显然是i=2时产生的3个3中左端点最大的,也即[3](而非[1,2],[1,2,3])

所以我们要维护的就是(或值,产生该或值的最大左端点)的kv对。那么最多有多少个这样的KV对呢?注意数据范围没有超过int类型,所以最多是32个或值(0个1,1个1,…,31个1)。这样我们开一个长度为32的静态数组去维护这个KV对集合即可。

class Solution {
    public int minimumSubarrayLength(int[] nums, int k) {
        int ans = Integer.MAX_VALUE;

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

        for(int i=0;i<nums.length;i++){
            ors[m][0] = 0;
            ors[m++][1] = i;

            int j = 0;
            for (int idx = 0; idx < m; idx++) {
                ors[idx][0] |= nums[i];
                if(ors[idx][0]>=k){
                    ans = Math.min(ans,i-ors[idx][1]+1);
                }

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

                ors[j][1] = ors[idx][1];
            }

            m = j+1;
        }

        return ans==Integer.MAX_VALUE?-1:ans;
    }
}

这里m相当于KV对集合的大小,而j代表了本次更新中遍历到的KV对,而

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

意味着,如果当前遍历到的KV对的或值不等于目前KV对的或值,说明我们找到了一个新的KV对,j往后移动,某则就要合并这两个或值相同的KV对了,取左端点的较大值,也即:

2.3. LC 1542 找出最长的超赞字符串

定义pre[i]为前i个数的异或和。

那么对于一个子串:

  1. 如果是偶数长度,那么开始位置i和结束位置j有:pre[i] = pre[j]
  2. 如果是奇数长度,那么pre[i]^pre[j] = 2^k(也即只有一个位置能是奇数个数)

而因为要求的是最长,那么显然可以利用哈希表维护达成某个pre的最早的索引。

对于任意索引i,ans = max(ans, i-pos[pre], max( i-pos[pre^(i<<d)] for d in range(10) ))

class Solution:
    def longestAwesome(self, s: str) -> int:
        alphabet = 10

        n = len(s)
        ans = pre = 0
        pos = [n]*(1<<alphabet) # 起初还未计算异或和对应最早的索引
        pos[0] = -1

        for i,x in enumerate(map(int,s)):
            pre ^= 1<<x
            ans = max(ans,i-pos[pre],max(i-pos[pre^(1<<d)] for d in range(alphabet))) # 偶数前后异或和相等,奇数可以选其中一个位置异或完不为0
            # 只需要最靠前的记录
            if pos[pre]==n:
                pos[pre] = i
        
        return ans

2.4. LC 3133 数组最后一个元素的最小值

把n-1插空插到x里面即可:

class Solution:
    def minEnd(self, n: int, x: int) -> int:
        n = n-1

        l = 0

        while n>0:
            if not (x>>l)&1: # 这一位为0,可以用
                x |= (n&1) << l # n的低位赋值给这一位
                n >>= 1
            l += 1
        
        return x

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值