java 位运算取8位_位运算:进阶技巧(下)

下篇会通过例题多介绍一些位运算的小技巧。首先回顾一下上篇最后的思考题:

  1. popcount在
    复杂性类里吗?答案是不在,所以我们目前的算法达不到
    也是情有可原的。证明不难,因为我们可以把parity(求输入中含有奇数还是偶数个1)归约到popcount,而parity不在
    复杂性类里的证明很多计算理论的课都会提到,原始论文参见[1]。
  2. reverse bits的更好算法?我好像不会... 但是这个操作显然在
    里。大家有好的想法可以在评论区里告诉我。

Total Hamming Distance

题意是给你n个二进制数,求他们两两之间的hamming distance之和。链接在这里:

https://leetcode.com/problems/total-hamming-distance/​leetcode.com
8573a5860df6952b33927c24c407a010.png

容易看出可以对每位分别统计,只要对每位求出所有输入数字中该位为0的个数和该位为1的个数就行。那么就有一个显然的

的算法。这也是我看到这题之前网上流传的唯一做法。

下面介绍如何用位运算做到

思路还是对每位维护一个计数器,表示(部分)输入的数中有多少个该位上为1。我在计算理论课上见过一个类似的题:设计一个电路把

个输入的bits加起来,和用二进制表示。这个电路只需要
个门(当时给的标准答案是
个门,不知道为什么),做法是分治,先递归求出前
个bits的和,可以用一个长度为
的计数器
表示,同理对后
个bits求出和的计数器
,然后用一个大小为
的电路算出
,其中
的长度为
。这个加法电路的实现如下:
void 

原理是从低位到高位一位位加起来,其中

是bool,表示
的第
位(
同理),
是进位。这也就是
的一种位运算实现,后文还会提到其他的实现方法。这个方法的劣势是电路延迟是
的,所以并不是cpu中的实现方法,但对于我们这题是适用的。

可以发现不需要改代码就可以用位运算做到对位并行计算,相当于我们用了

的时间并行做了
位的
,所以延迟在这里不是问题。

解递归式可得,整个电路的复杂度是

我的代码在这里:477. Total Hamming Distance.cpp

225a8c2ac3e7fb7e64fdd5107095999d.png

p.s. 这题还有一个

的做法,是 @沈洋 教我的:如果每个输入的数只有
个bits,那么一共只有
种输入的数,分别把每种数的出现次数用桶统计出来就好。取
s.t.
为任意小于1的常数,把输入按位分块(一共需要做
轮),然后随便用个关于
或者
的算法就行,复杂度慢点也没关系(实际也有个
的优美写法)。虽然理论复杂度略差但是实际跑得飞快。

bitset高精度

我在知乎看到过这个问题:

如何使用bitset实现高精度?​www.zhihu.com

抱着猎奇的心态找了一下实现,比如 高精度wu-kan。我修正了链接中写法的一些问题并优化了一下复杂度。下面介绍一下各种运算的实现方式:

位运算(&, |, ^, ~, <<, >>):bitset原生支持,复杂度

加法:和之前提到的写法有一点点类似,用位运算处理进位,每轮把a+b归约到a^b和(a&b)<<1的加法,其中a^b是不考虑进位的和,a&b是进位部分,需要左移1位。因为最坏情况下会做

轮(111...11+000...01),每轮的位运算需要
的时间,复杂度最坏
。实际表现会比这个快一点,因为如果
是从
中独立均匀随机抽出的整数,不难证明期望需要
轮停止,复杂度是
(下文把这个叫作平均复杂度)。
Bint 

当然也可以用正常高精度加法的写法做到

,但是那样就失去了猎奇的意义,代码会变长一点点,并且bitset里的数据是private的,封装好了不能直接操作,需要用一些不科学的手段(比如指针)绕过去。

减法:归约到加法,在补码表示下取反+1就可以得到

Bint 

但这里有个小小的问题:因为一个很小的数取反之后高位会有一串前导1,归约到加法之后平均复杂度的分析失效了。为了达到

的平均复杂度还是得像加法那样写(注意后面的除法和输出会用到大量减法,所以优化减法还是很重要的):
Bint 

比较:一位位比就行,复杂度

。一下子没想到优美的
的写法。
bool 

乘法:用竖式乘法,归约到

个加法,最坏复杂度
,平均复杂度大概为
(我没仔细验证最坏情况能不能达到以及平均复杂度能不能按照之前那个加法的结论用类似方法推出,不过看起来是这样)。
Bint 

除法、取模:用长除法,归约到

个比较、加减法和位运算,最坏复杂度
,平均复杂度大概为
pair

输出:如果要十进制结果的话需要做一次

进制到十进制的进制转换。可以直接调用取模(%10),但会变得极慢。稍微快一点的写法是用减
代替取模,不断减小
把答案逐位求出来。需要调用
次减法、乘常数和比较,最坏复杂度
,平均复杂度大概为

完整代码如下:

#define N 2048

总结:用bitset实现高精度的代码会比朴素的正常写法短一些,运算速度比正常写法慢一点(大概差几十倍,但极端情况下会被卡),优点是因为在

进制下操作,可以同时支持位运算及算术运算。瓶颈在输出,所以不适合需要大量输出的题。

a+b

题意是计算

,但不允许使用+和-运算符。链接在这里:
Loading...​leetcode.com
8573a5860df6952b33927c24c407a010.png

(一般来讲这种限制不能用xx操作的面试题没什么意思。但这可是著名难题

啊,解法不嫌多)

刚刚已经介绍了一个

复杂度的逐位实现,这也是一般面试者会给(或被期望给出)的答案。

用简单的分治策略可以做到

因为补码表示的性质,不妨假设

均非负。分
个长度为
的块。用
的时间可以判断在计算
时第
块的最低位上是否会接收右边传来的进位:令
为从第
块的最低位的右边一位开始到第0位对应表示的数,那么只需判断
是否会溢出。这当且仅当
(用&把取反之后多余的前导1截断)。

对每块算出最低位上是否会接收右边传来的进位之后,块之间的加法结果就互相独立了,可以并行计算。用高精度bitset的加法算法就行,每轮把a+b归约到a^b和(a&b)<<1的加法,但用&来避免跨块进位。在

轮之后就会结束。跨块进位部分再做一次加法就行。

还有一种更巧妙的做法:又又又是利用乘法。根据卷积的意义有

,乘法结果的中间一段位含有
。把
按照高低位分两段(避免
的存储位置溢出),分别用乘法求和,再调用一次加法处理下进位就行。复杂度
。代码如下:
uint 

DFA

Single Number II

题意:输入的

个数中有1个出现了1次,其他数都恰好出现了3次,找出那个恰好出现1次的数。要求使用
的额外空间。

题解:一个很自然的想法是对每位分别处理,设计一个包含

个状态的DFA来求出某位上所有数的和mod 3,每读入一个数进行一次转移,这样那些出现3次的数就互相抵消了。DFA的转移可以写成
,表示当前状态为
,读入一个值为
的数时转移到状态
。转移函数可以表示成关于
的布尔函数,所以可以用
次位运算模拟DFA的转移并支持并行。这样就可以做到
了。当然这个设计思想比较暴力,仔细优化一下的话可以得到非常简洁的代码。

BZOJ2908:又是nand (权限题,想看原题面可以戳这里)

题意:定义A nand B=not(A and B)。给出一棵树,树上每个点都有点权,定义树上从a到b的费用为0与路径上的点的权值顺次nand的结果。需要支持以下操作:

1. Replace a b: 将点a (

) 的权值改为b。

2. Query a b: 输出点a到点b的费用。

题解:注意到nand不满足交换律和结合律。但是如果把每位分开计算,可以用一个DFA(或者2*2的矩阵)来表示一段链的左边(右边同理)来了一个0或1之后依次nand链上所有点权得到的0/1值,这样就有结合律了。然后可以用树链剖分/动态树维护。直接做的话每位的答案都要单独计算,复杂度

。网上大部分题解是这样写的。

用刚刚讲的位运算模拟DFA转移的方法可以把复杂度除一个

,做到
。rank1(964ms)的代码现在还是我的(6年过去了!),用的是Global Balanced Tree(静态的动态树),常数会小一点。位运算核心部分代码:(当时应该是从Seter那学的)
struct 

p.s. 以前我在校内模拟赛给别人出过这个题,结果当时造数据的时候用了随机权值(树的形态不是随机的),被一堆人水过了。大家可以思考一下这个情况下的简单做法。

矩阵转置

先介绍一对用乘法可以做到的小技巧:

1. (pack) 假设一个

位二进制数被分成了
块(
),每块长度为
,块内最后
位存了一个数(把第
块里的数叫作
),其余部分为前导0。排列如下图:

这个数乘

(一共
个1,两个1之间隔着
个0)之后就可以在最高位部分得到连续的一段

2. (unpack) 即上面那个的逆操作,把

变成
。用
(一共
个1,两个1之间隔着
个0)再用&把无关位置置0就可以得到
,额外处理下
就好。

这两个操作可以用来对一个64位整数表示的4*4矩阵(每个数4bit)进行快速转置,只需取

,
。可以看这篇文章:
rlei:位运算hack: 快速矩阵转置​zhuanlan.zhihu.com
7518c67010d55c52eaa5d32a63f41cd2.png

代码:

void 

p.s. 转置也可以用类似reverse bits的方法做,不需要乘法。

印象中pack/unpack在另外一些问题中也有用,什么时候记起来了再放个例题。

update.

  1. 翻书看到了The Art of Computer Programming[2]的7.1.3节中介绍了一个在
    时间内对二进制位任意重排列顺序的算法,并且不需要用到乘法。更一般地,我们可以在
    时间内对二进制位作任意映射
    ,其中
    为任意
    的映射[3]。上文提到的reverse bits/矩阵转置可以看成是这个一般化结论的特殊情况。

2. 同一章中给出了一些lower bounds,例如reverse bits在不使用乘法的情况下需要

的时间。

References

[1] Furst M, Saxe J B, Sipser M. Parity, circuits, and the polynomial-time hierarchy[J]. Mathematical systems theory, 1984, 17(1): 13-27.

[2] Knuth D E. The art of computer programming, volume 4A: combinatorial algorithms, part 1[M]. Pearson Education India, 2011.

[3] Chung K M, Wong C K. Construction of a Generalized Connector with 5.8 n log 2 n Edges[J]. IEEE Transactions on Computers, 1980 (11): 1029-1032.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值