位运算及其对程序的优化.docx
《位运算及其对程序的优化.docx》由会员分享,可在线阅读,更多相关《位运算及其对程序的优化.docx(28页珍藏版)》请在冰豆网上搜索。
位运算及其对程序的优化
位运算及其对程序的优化
常州市第一中学戴涵俊
JSOI2009省队论文
目录
序言…………………………………………………………………………3
正文…………………………………………………………………………4
一、位运算的基本操作…………………………………………………4
1.位运算介绍
2.位运算的优先级
3.位运算的口诀
二、位运算的实用技巧…………………………………………………5
1.对于mod运算的优化
2.位运算的一些技术
三、位运算对一些数据结构的优化……………………………………6
1.循环队列
2.树状数组
3.集合
4.哈希表
四、位运算对一些算法的优化…………………………………………12
1.状态压缩动态规划
2.搜索
五、总结…………………………………………………………………15
六、附录…………………………………………………………………15
七、参考资料……………………………………………………………24
序言
程序中所有的数据在计算机内存中都是以二进制的形式储存的。
位运算,本质上就是直接对整数在内存中的二进制位进行运算,同时,数的各个二进制位互不影响。
由于位运算直接对内存数据进行操作,不需要转换成十进制,因此处理速度非常快,在信息学竞赛中往往可以优化理论时间复杂度的系数。
另外,位运算还有很多特殊的技巧,能够帮助我们简化代码、美化程序等等。
本文就结合自己的学习和应用经验,介绍一些位运算及其对程序的优化方法。
正文
一.位运算的基本操作
1.位运算介绍
①and
X
Y
XandY
0
0
0
1
0
0
0
1
0
1
1
1
通过上表不难发现,只有当x和y都为1时and后值才为1。
and运算主要是用来取出某个二进制位。
例如:
Aand(1shl5)就是取出A的二进制数从右往左第6位。
②or
X
Y
XandY
0
0
0
1
0
1
0
1
1
1
1
1
or运算通常用来强行给二进制的某一位赋值,注意or运算可能导致变量越界,对于有符号类型,or可能把符号位取反,无符号类型可能直接就变成0了,这有可能会导致数据的丢失,使得程序崩溃。
因此,执行or运算,应该尽量保证变量不超界,或者更保险地,是非负数。
3xor
X
Y
XandY
0
0
0
1
0
1
0
1
1
1
1
0
当两个位不同时得到1,否则为0。
因此xor通常可以用来取反。
有意思的是xor的逆运算是其本身。
于是,我想起了一道做过的有点诡异的题目:
给你n个数,n大到只保证你读入不超时,其中仅有一个数出现了奇数次,要你找出这个数。
方法是只要通过读一个xor一个,出现偶数次的肯定抵消了,剩下来的就是那个数了。
4not
X
not(X)
0
1
1
0
not操作就是直接把内存中的0和1全部取反。
对于有符号类型,符号位也会取反,这是需要注意的,比如x=2147483647,x:
longint;not(x)就得到了-2147483648,即011111……1是表示maxlongint,100000……0是表示的-maxlongint-1。
5shl
左移,就是把二进制数整体向左移动x个位,并且右数x个单位是0;如果移出界,那么移出部分就丢失了,而不会runtimeerror。
对于有符号类型,移位当然会移到符号位上去,比如x=2^30,x:
longint;那么xshl1就得到-2147483648;
6shr
右移,就是把二进制数整体向右移动x个位,原来的最高x个位就变为0;shr相当于div2。
2.位运算的优先级
not>and,shl,shr>or,xor
比如下面的几个运算:
not1or1=-1
not1and1=0
1and1shl1=2
not1or0shl1xor0and1=-2
虽然掌握各个运算符的优先级并不困难,但是为了避免出错,增强程序的可读性,利于调试,我们还是在需要的地方添加括号来保证优先运算。
3.位运算的口诀
清零取反要用与,某位置一可用或。
若要取反和交换,轻轻松松用异或。
4.举例
例1、一个文件中有9亿个不重复的9位整数,现在要求对这个文件进行排序(当然时间可以不止1秒,但要求出可行解)。
[问题分析]
拿到题目也许会吓一跳,这么多数,就是想快排存也存不下啊。
线性时间的排序算法—桶排序?
貌似是这样,但是9位数,这么大的哈希数组也弄不下啊。
注意到每个数都不同,那么每个数顶多出现1次,也就是说,要么出现,要么不出现。
那么,我们可以用0表示未出现,1表示出现。
当然,这里一个longint不够,那么就10^9div32+1个longint,对于一个数,判断它是在哪一个longint的哪一位,把它变成1。
输出时当作哈希表一样处理就可以了,总的时间复杂度是O(n),比爆空间且缓慢的快排、裸的桶排序等算法要好的多。
二.位运算的实用技巧
1.对于mod运算的优化
一些特殊的模运算可以用位运算来代替,比如模2的整数次幂,我们可以用and运算来代替mod运算,程序段如下(以mod1048576为例,y=134328497)。
mod版本:
fori:
=1totimedox:
=ymodbase;
and版本:
fori:
=1totimedox:
=yand(1shl20-1);
当time分别取10000000,100000000,1000000000的时候,各自的用时如下:
mod
and
10000000
0.22s
0.14s
100000000
1.26s
0.52s
1000000000
12.00s
4.23s
上面是在我的酷睿T2370笔记本上测试的,用go32v2编译执行。
虽然不同电脑,不同编译器结果可能不同,但是,我们不难发现,改成and之后,效率有了较大的飞跃,而且运算次数越多,效率提升越明显。
2.位运算的一些技术
运算要求
用位运算实现
实际应用
把右数第k位强行赋为1
xor(1shl(k-1))
通过这些操作,我们可以用01表示某个状态并且方便地改变这一状态,在哈希表、状态DP、一些搜索记录状态中很实用
把右数第k位强行赋为0
xandnot(1shl(k-1))
右数第k位取反
xxor(1shl(k-1))
去掉右起第一个1的左边
xand(xxor(x-1))
树状数组中用到的低位技术
求x和y的平均值下取整
xandy+(xxory)shr1
避免x+y超界但结果不超界的情况
求x的相反数(x基类型为有符整型)
notx+1
更好地理解not以及数在计算机中的存储方式
交换变量x和变量y的值
x:
=xxory;y:
=xxory;
x:
=xxory;
省去交换变量时候需要的临时变量
用位运算取绝对值(以32位整数为例)
xxor(not(xshr31)+1)+xshr31
三.位运算对一些数据结构的优化
1.循环队列
循环队列比较方便的实现可以用一个头指针head,一个尾指针tail,每次取出来的是headmodbase。
这里不妨把base设置为2的整数次幂-1,然后用and来取模,既方便了代码书写,又不会降低效率,还美化了程序。
举一个例子,SPFA是大家熟悉的求最短路径的简单、高效的算法,它需要维护一个队列,并且队列中最多会有n个元素(n为顶点数)。
于是,这里的队列我们可以用位运算优化的循环队列来做,代码见附录1。
2.树状数组
这种数据结构可以用线段树来代替,但是线段树比较烦琐,并且理论时间复杂度系数比较大,而树状数组就比较好写,且系数比较小。
关于树状数组的具体原理就不赘述了,这里讲讲它的关键技术——低位技术(Lowbit),树状数组需要知道一个数的二进制表示中从右往左第一个1的位,用位运算实现只要执行:
L[i]:
=iand(ixor(i-1))。
3.集合
用一个二进制数来表示一个集合的状态,用来替代PASCAL中缓慢的“集合”这种数据结构,可以大大提高程序的运行效率。
因为每次位运算的操作可以看作是O
(1)的;同时,这种表示方法也利于编程,我们可以方便地用一个二进制数来表示。
唯一的缺点就是,数感不好的同学不能一眼看出一个十进制数的某一位是1还是0。
下面列举了一些集合的操作用位运算来实现的方法。
功能
用位运算实现
集合的并、交、差
AorB;AandB;AandnotB
添加/删除某一个元素
Aor1shl(k-1);Aandnot(1shl(k-1))
判断某一元素是否存在
Aand(1shl(k-1))是否为0。
为0就是不存在
从A集合中删去B集合
(B集合为A集合的子集)
AxorB
例2、PIGS
问题描述:
尼克在一家养猪场工作,这家养猪场共有M间锁起来的猪舍,由于猪舍的钥匙都给了客户,所以尼克没有办法打开这些猪舍,客户们从早上开始一个接一个来购买生猪,他们到达后首先用手中的钥匙打开他所能打开的全部猪舍,然后从中选取他要买的生猪,尼克可以在此期间将打开的猪舍中的猪调整到其它开着的猪舍中,每个猪舍能存放的猪的数量是没有任何限制的。
买完猪后客户会将他打开的猪舍关上。
好在尼克事先知道每位客户手中有哪些钥匙,要买多少猪,以及客户到来的先后次序。
请你写一个程序,帮助尼克求出最多能卖出多少头生猪。
输入格式:
输入文件的第一行包含两个整数M和N,1≤M≤1000,1≤N≤100,M为猪舍的数量,N为客户人数,猪舍的编号为1到M,客户的编号为1到N。
输入文件第二行包含M个空格隔开的整数,依次表示每个猪舍中的生猪数量,每个整数大于等于0,且小于等于1000。
接下来的N行每行表示一位客户的购买信息,第I个客户的购买信息位于第I+2行,其格式如下:
AK1K2……KAB
它表示该客户共有A把钥匙,钥匙编号依次为K1K2……KA,且K1输出格式
输出文件仅有一行包含一个整数,表示尼克最多能卖出的生猪的头数。
样例:
PIGS.IN
33
3110
2122
2133
126
PIGS.OUT
7
[问题分析]
看到每个猪舍有数量限制、每个人有需求量上限,我们不难想到网络流的模型。
首先可能会想到一个比较裸的模型,就是先一个源点向m个猪舍连边,上限为猪的数量,然后第一个客户向可以取到的猪舍连边,上限为正无穷,并且他向汇点连边,上限为需求量;第一个客户再伸出m条边重新引出m个猪舍,再作为新的源点向第二个客户连边,以此类推。
这个图的层次、点数都相当多,显然,我们要进行优化。
因为可以把一个猪舍的猪趁打开猪舍时候赶到另外一个猪舍去,于是两个不同的客户如果有相同的猪舍可以开,就可以共享他们所能开到的所有猪舍的猪。
于是我们可以直接对两个客户连边,即,如果客户x和y,满足x问题似乎到这里就完了,其实不然。
由于要对任意两个人进行判集合的交是否为空,那么复杂度是100*100,然后要对所有的猪舍进行比对,这个复杂度是1000,于是,这个预处理的理论复杂度上限可能是O(10^8),虽然经试验下来不太可能达到这个复杂度,但是我们还是要用一种简单易行的方法进行优化——位运算。
这里用整数来表示猪舍的有无情况,这样最大可以有1000个二进制位,于是还要分段处理。
不妨每段用一个64位的qword,把1000总共分成16段,对于每个猪舍编号k,先判断是哪一段的(用(k-1)div64+1,可以写成shr6加速),然后再在那一段进行赋值。
于是,对于两段猪舍,看有无交集,只需要and一下,看是不是0就可以了。
这样,我们把预处理的时间复杂度降到了O(100*100*16)。
虽然本题用位运算做有点显得多余了,但是这种方法还是有推广意义的。
例3、山贼集团
问题描述:
某山贼集团在绿荫村拥有强大的势力,整个绿荫村由N个连通的小村落组成,并且保证对于每两个小村落有且仅有一条简单路径相连。
小村落用阿拉伯数字编号为1,2,3,4,…,n,山贼集团的总部设在编号为1的小村落中。
山贼集团除了老大坐镇总部以外,其他的P个部门希望在村落的其他地方建立分部。
P个分部可以在同一个小村落中建设,也可以分别建设在不同的小村落中。
每个分部到总部的路径称为这个部门的管辖范围,于是这P个分部的管辖范围可能重叠,或者完全相同。
在不同的村落建设不同的分部需要花费不同的费用。
每个部门可能对他的管辖范围内的小村落收取保护费,但是不同的分部如果对同一小村落同时收取保护费,他们之间可能发生矛盾,从而损失一部分的利益,他们也可能相互合作,从而获取更多的利益。
现在请你编写一个程序,确定P个分部的位置,使得山贼集团能够获得最大的收益。
输入格式:
输入文件第一行包含一个整数N和P,表示绿荫村小村落的数量以及山贼集团的部门数量。
接下来N-1行每行包含两个整数X和Y,表示编号为X的村落与编号为Y的村落之间有一条道路相连。
(1<=X,Y<=N)
接下来N行,每行P个正整数,第i行第j个数表示在第i个村落建设第j个部门的分部的花费Aij。
然后有一个正整数T,表示下面有T行关于山贼集团的分部门相互影响的代价。
(0<=T<=2p)
最后有T行,每行最开始有一个数V,如果V为正,表示会获得额外的收益,如果V为负,则表示会损失一定的收益。
然后有一个正整数C,表示本描述涉及的分部的数量,接下来有C个数,Xi,为分部门的编号(Xi不能相同)。
表示如果C个分部Xi同时管辖某个小村落(可能同时存在其他分部也管辖这个小村落),可能获得的额外收益或者损失的收益为的|V|。
T行中可能存在一些相同的Xi集合,表示同时存在几种收益或者损失。
输出格式:
输出文件一行包含一个数Ans,表示山贼集团设置所有分部后能够获得的最大收益。
样例数据:
输入样例
输出样例
21
12
2
1
1
311
5
数据规模:
对于40%的数据,1<=P<=6。
对于100%的数据,1<=N<=100,1<=P<=12,保证答案的绝对值不超过108。
[问题分析]
本题还是有点意思的状态压缩树型动态规划,首先,我们用一个集合表示当前村落已经驻扎的山贼情况(这个可以用二进制数来表示),那么状态转移方程就可以如下表示:
f[i,j]:
=max{f[i,x]+f[k,y]}+profit[j],其中i表示子树的根节点,k表示儿子节点,j表示根节点的状态集合,xory=j,profit为对应状态下的总的盈利值。
由上,我们需要快速知道给定的一个集合,它的所有子集是什么,如果纯粹地对每个集合进行暴力穷举,这个时间复杂度是4^n,但是,我们知道一个结论,就是n个元素的全集,它的所有子集的所有子集个数总和为3^n(这个比较容易证明,在此不再赘述)。
所以,我们希望不要穷举没用的集合。
一种方式比较容易想到,就是预处理每一个集合各自的子集,并用拉链存储。
预处理时候再用队列优化,预处理用n*3^n。
在当时我就是这么做过的。
另外有一种更好的方式,不需要预处理,在参考了NOI专刊上的论文后感觉它与lowbit技术很好地结合了起来。
比如当前的集合为A,那么我们依次枚举出它的子集:
B:
=A;
WhileA>0do
BeginA:
=(A-1)andB;End;
A-1的意思就是把A集合中最后一个1变成0,最后一个1后面的0都变成1(最后是指右边),然后再and一个B,保证是原来A的子集。
这样做就相当方便了。
4.哈希表
哈希表最关键的莫过于哈希函数了,一个好的哈希函数可以大大降低哈希过程中的冲突,一般的哈希函数就是mod一个大质数。
其实,一些用位运算写的哈希函数能够获得不错的效果,华丽的位运算也体现了它独特的魅力。
下面就介绍几个优秀的字符串哈希函数。
//DJBHash
functionDJBHash(s:
string):
un;
varhash:
int64;
i:
longint;
begin
hash=5381;
fori:
=1tolength(s)dohash:
=hash+(hash<<5)+ord(s[i]);
DJBHash:
=hashandbase
end;
//APHash
functionAPHash(s:
string):
un;
varhash:
int64;
i:
longint;
begin
hash:
=0;
fori:
=1tolength(s)do
ifodd(i)thenhash:
=hashxor(hash<<7)xorord(s[i])xor(hash>>3)
elsehash:
=hashxornot((hash<<11)xorord(s[i])xor(hash>>5));
APHash:
=hashandbase
end;
//SDBMHash
functionSDBHash(s:
string):
un;
begin
hash:
=0;
fori:
=1tolength(s)dohash:
=ord(str[i])+(hash<<6)+(hash<<16)-hash;
//equivalentto:
hash=65599*hash+ord(str[i])
SDBHash:
=hashandbase
end;
//JSHash
functionJSHash(s:
string):
un;
begin
hash:
=1315423911;
fori:
=1tolength(s)dohash:
=hashxor((hash<<5)+ord(str[i])+(hash>>2));
JSHash:
=hashandbase
end;
上面的哈希函数原理就不再详细阐述了,重要的是运用。
用上述哈希函数来优化哈希表,可以使你的程序得到进一步优化——华丽中彰显实用。
例4、cryptcow(USACO4.1)
问题描述:
农民Brown和John的牛们计划协同逃出它们各自的农场。
它们设计了一种加密方法用来保护它们的通讯不被他人知道。
如果一头牛有信息要加密,比如"InternationalOlympiadinInformatics",它会随机地把C,O,W三个字母插到到信息中(其中C在O前面,O在W前面),然后它把C与O之间的文字和O与W之间的文字的位置换过来。
这里是两个例子:
InternationalOlympiadinInformatics->CnOIWternationalOlympiadinInformatics
InternationalOlympiadinInformatics->InternationalCinInformaticsOOlympiadW
为了使解密更复杂,牛们会在一条消息里多次采用这个加密方法(把上次加密的结果再进行加密)。
一天夜里,John的牛们收到了一条经过多次加密的信息。
请你写一个程序判断它是不是这条信息经过加密(或没有加密)而得到的:
BegintheEscapeexecutionattheBreakofDawn
输入格式:
一行,不超过75个字符的加密过的信息。
BegintheEscCutionattheBreOapeexecWakofDawn
输出格式:
一行,两个整数.如果能解密成上面那条逃跑的信息,第一个整数应当为1,否则为0;如果第一个数为1,则第二个数表示此信息被加密的次数,否则第二个数为0。
11
[问题分析]
本题是比较明显的搜索题,但是,如果不加一点点优化是无法在有效时间里面出解的。
其中之一的优化便是哈希判重了,即,已经搜索过的状态就没有必要再搜索了。
对于字符串的哈希我们不妨使用上面几个强大的哈希函数来进行。
其余的就不多说了。
四.位运算对一些算法的优化
1.状态压缩动态规划
这里的状态是可以用集合来表示的状态,其实上面一个例子已经讲到用位运算来做状态压缩动态规划了,下面再举一个例子。
例5、BOND
【问题描述】
所有人知道秘密特务007,詹姆斯·邦德,但是很少人知道,许多时候他并不亲自去做任务,而是让他的表兄弟们完成。
现在每当詹姆斯收到任务,他就将任务分发给大家,于是他需要你的帮助。
詹姆斯每月收到一张任务单。
对于每一个任务,他都根据以往的经验,计算出他和其他几位兄弟完成的成功概率。
你的程序应当找到一种分配方案,使得所有任务都被成功完成的概率最大。
注:
所有任务都被成功完成的概率,等于每个任务都被成功完成的概率之积。
【输入格式】
第一行包含一个整数N(<=20),表示有N个任务,而他有N个兄弟来替他完成。
接下来包含N行,每行包含N个0到100之间的整数。
第i行第j列的数,表示第i个任务被第j个兄弟完成的成功概率。
概率以百分比的形式给出。
【输出格式】
仅一行,输出所有任务都被成功完成的最大概率,要求误差不超过±10-6。
【测试样例】
bond.in
2
100100
5050
bond.out
50.000000
【问题分析】
本题还是比较明显的状态压缩动态规划,n<=20。
我们可以按照任务被完成情况来划分状态,f[i,j]表示完成到第i个任务为止,人员使用情况为j的时候所能获得的最大概率。
显然,这个状态划分满足最优子结构和独立性。
对于状态j,我们可以方便地用每个二进制位上的数是0还是1来表示某一个人有没有被用。
于是状态转移方程如下:
F[i,j]:
=max{f[i-1,k]*a[i,r]|kor(1shl(r-1))=j且kand(1shl(r-1))=0};
我们发现这个方程如果直接拿递推写还有点麻烦,于是想到用队列优化,这样不仅可以方便程序实现,还剔除了一些冗余状态。
实际测试下来还超时一个点,标准算法是用费用流或者km写的,这里就不再展开讨论。
2.搜索
对于搜索的优化,可以有多个方面,同样,位运算可以从多个方面对搜索进行优化。
(1)状态表示
下文例题中的TV一题,便是通过用二进制数结合位运算来表示状态并进行状态转移,这样不仅提高了搜索效率,而且还方便了程序实现。
(2)状态判重
搜索常常需要开一个哈希表来记录状态是否达到或者该状态的最优值,有了位运算,一方面,我们将状态表示成整数,通过直接寻址或者拉链储存状态;另外一方面,加入位运算的哈希函数更加强大,减少哈希过程中的冲突。
例6、TV
源程序名 TV.?
?
?
(PAS,BAS,C,CPP)
可执行文件名TV.EXE
输入文件名 TV.IN
输出文件名TV.OUT
运行时间限制3S
问题描述