• 从零学算法(LCR 178)


    教学过程中,教练示范一次,学员跟做三次。该过程被混乱剪辑后,记录于数组 actions,其中 actions[i] 表示做出该动作的人员编号。请返回教练的编号。
    示例 1:
    输入:actions = [5, 7, 5, 5]
    输出:7
    示例 2:
    输入:actions = [12, 1, 6, 12, 6, 12, 6]
    输出:1

    • 乍一看很简单,某个数字只出现一次,其他数字都出现三次,找出只出现一次的数字。直接用 HashMap 统计每个数字出现的次数即可,不过可以用位运算更快实现。思路为:这些数字都是 32 位整数,那就用一个长度 32 的数组记录每一位 1 的个数。比如三个 1001 和一个 1000,最后会得到数组[4,0,0,3…],表示经统计,第一位为 1 的数字共有 4 个,第四位为 1 的有 3 个,去掉三的倍数个 1,剩下的就是只出现一次的二进制数字每一位的情况。也就从 4003 成了 1000,也就是只出现一次的那个数字。
    •   public int trainingPlan(int[] nums) {
            int[] res = new int[32];
            // 统计每一位上的 1 共有几个
            for(int num : nums){
                for(int i=0;i<32;i++){
                    res[i] += num & 1;
                    num>>=1;
                }
            }
            // 统计完了就得想办法把这个数组还原成二进制数
            int ans = 0,m = 3;
            for(int i=0;i<32;i++){
            	// 从头开始遍历,对 3 取余后的数字,每一位对应的位置应该是 x << i
            	// 比如 res 取余 3 后为 1001,ans 的变化过程为 
            	// 0000 | 1<<0 = 0001
            	// 0001 | 0<<1 = 0001
            	// 0001 | 0<<2 = 0001
            	// 0001 | 1<<3 = 1001
            	// 所以 <
                ans |= res[i]%3<<i;
            }
            return ans;
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
    • 上面一种解法理解起来相对比较容易,根据同样的思路,有更快的解法,不过具体实现有点难以理解。我只能以我的理解来讲解。首先根据上面的思路,一堆 32 位整数在“相加”(懂我说的相加吧,1001+1100=2101,这个相加是统计每一位有几个 1)的过程中由于最后要对 3 取余,所以实际上每一位(注意是每一位)会经历的只有三种状态变化的可能性。规律为 0->1->2->0->1->2…。由于每一位都是同样的变化规律,所以我们先单独分析一位。因为最大会变化成 2 ,所以一个二进制位无法表示,我们用两个二进制位来表示某一位(再次提醒,这个某一位表示的是相加过程中 32 位结果中对应的某一位的统计结果,就是上面解法中的某个 res[i]),我们为这两个二进制位起名为 two 和 one,00,01,10 就分别表示 0,1,2 这三种状态。那么我们重新表示一遍我们状态变化的规律为 00->01->10->00->01->10…。接下来我们先用最原始的判断来表述一遍 one 遇到每个 num 会怎么样(省事点先用 python 了):
    • 在此之前先附上完整的真值表:记录某一位状态的 two one 加上对应位的 n 后的计算结果,two one 共有三种状态,n 为 0 或 1,所以共有 6 种情况
      请添加图片描述
    •   # 其实这也已经简化过了,没有划分每种 two, n, one 最终计算出的对应的 新one
        if two==0:
        	if n==0: #记得这个 n 表示的是在 32 位的某一位上的数,因为 one two 也是用来表示某一位相加过程中的状态
        		# 你也可以自己划分一下 if one== 0,if one == 1,会发现最后结果都是 one
        		one = one
        	if n==1:
        		one = ~one # one 的相反数
        if two==1:
        	# 此时 two one 只可能为 10,那么你 n 为 0 ,one 还是保持为 0,n 为 1,我就得变成 00,one 还是 0
        	# 也就是说 n 首先此时必定为 0,并且无论变或不变最后都成了 0
        	one = 0 
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 由于实际上最终状态只会剩下 00 和 01,换言之,因为 two 恒为 0,所以是 one 真正记录了最后的两种状态,所以我们返回 one 就能表示 32 位结果中对应的某一位最后为 0 还是 1
    • 我们通过对上面代码的简化优化成位运算,那么最终我们就得到了每一位的批量计算,因为每一位的计算规则都一样(你需要理解这点,这也是为什么我们先分析单独的一位)。
    • 我们先简化 two 为 0 的情况,列出 n 与 one 的真值表
    n  one newone
    0  0   0
    0  1   1
    1  0   1
    1  1   0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 可以看到符合异或运算,即
    •   if two==0:
        	one = one^n
        if two==1:
        	one = 0 
      
      • 1
      • 2
      • 3
      • 4
    • 接着简化,老样子,真值表,这里把 one^n 暂时记作 one
    two one newone
    0   one  one   
    0   one  one
    1   one  0
    1   one  0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 这个就稍复杂一点点,x?0=x, x?1=0,我们知道 x&1=x, x&0=0,但是这里正好相反,是 0 和 1,因为 ~0=1,~1=0 ,那么不难想到了, x&~0=x, x&~1=0,所以我们就得到了 newone = one &~two,把 one^n 再代入回来,我们最终得到了
    • one = (one^n) & ~two
    • 我们还得根据计算出的 one 继续计算出 two,按道理我们是需要根据旧的 one 来加 n 计算出 two,那我们还是一样先真值表,根据旧的 two 和 n 以及 新的 one,推算出旧的 one,然后得到新的 two
      请添加图片描述
    • two old:当前 two 的值
    • 第一行:two 为 0,n 为 1,新的 one 为 1,说明之前的 two,one 为 00,(two,one 我就简称为 x 吧),所以 00+1 = 01,新的 two 为 0;
    • 第二行:two 为 0,n 为 1,新的 one 为 0,说明之前的 x 为 01,01+1=10,新 two 为 1
    • 第三行:two 为 1,n 为 1,新 one 为 0,说明之前的 x 为 10,其实 two 为 1,x 只可能是 10,10+1=00,新 two 为 0
    • 不用我继续写了吧,根据真值表我们先得到最朴素的 6 个 if
    •   if two==0:
        	if n==0:
        		if one == 0:
        			two = two
        		elif one == 1:
        			two = two
        	if n==1:
        		if one == 0:
        			two = 1 #~two
        		if one == 1:
        			two = two
        elif two==1:
        	if n==0:
        		if one == 0:
        			two = two
        	if n==1:
        		if one == 0:
        			two = 0 #~two
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    • 简化一下,结果还是为 two 的情况可以省略:
    •   if two==0:
        	if n==1:
        		if one == 0:
        			two = 1
        elif two==1:
        	if n==1:
        		two = 0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 继续简化:
    •   if two==0:
        	if n==1 && one == 0:
        		two = 1
        elif two==1:
        	if n==1 && one == 0:
        		two = 0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 再简化,为了使得 n==1 && one == 0 为 true,你会发现可以转换成 n&~one
    •   if two==0:
        	if n&~one:
        		two = 1
        elif two==1:
        	if n&~one:
        		two = 0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 到了这里你不觉得有点眼熟吗,你可以扩充完整来列出真值表
    •   if two==0:
        	if n&~one:
        		two = 1 # 这个 two 就是 newtwo
        	else:
        		two = 0
        elif two==1:
        	if n&~one:
        		two = 0
        	else:
        		two = 1
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • 然后得到了:
    two n&~one newtwo
    0   0      0   
    0   1  	   1
    1   0      1
    1   1      0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 这不就是异或吗,所以 two = two ^ (n&~one),因为 & 优先级高于 ^ 所以小括号可以省略
    • 最终代码如下
    •   public int trainingPlan(int[] nums) {
        	// 因为是批量位运算,所以用了 ones 而不是 one
            int twos = 0, ones = 0;
            for(int n:nums){
                ones = (ones^n) & ~twos;
                twos = twos ^ n & ~ones;
            }
            return ones;
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 我只有一个疑问,为什么别人的解法最终是:
    •   public int trainingPlan(int[] nums) {
            int twos = 0, ones = 0;
            for(int n:nums){
                ones = ones^n & ~twos;
                twos = twos ^ n & ~ones;
            }
            return ones;
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
  • 相关阅读:
    基于51单片机的智能路灯控制系统(lunwen+设计说明+仿真+程序)
    习练真气运行法必须从调整呼吸入手
    IDEA 中配置 Gradle 和使用
    内网开发之后端配置前端项目流程
    解决在uniapp中自定义组件onLoad回调不执行
    『现学现忘』Docker命令 — 18、镜像常用命令
    AOP事务管理
    LT1931
    【Python机器学习实战】----基于AdaBoost分类树模型、梯度提升分类树模型、Bagging分类树模型以及随机森林分类模型对空气质量等级进行预测
    LeetCode-946-验证栈序列
  • 原文地址:https://blog.csdn.net/m0_53256503/article/details/133277699