【转载】思考题:位运算加速(一)位并行
2026-07-30 17:13:33
发布于:安徽
引入
并行是现实世界中加速计算的基本方法。其中,在 OI 这样的单线程程序中也十分实用的基本操作,就是用单条指令处理多组数据。这种思想中最基本的操作就是位运算,将数据中的一个字[word]看成同时存储和处理多个位的方式。
问题0. 请你自行了解常见的位运算指令的作用。(包括一些 __builtin 等)
并行处理多个位
问题1.1. 输入两个数组 a[32], b[32],它们分别表示两个二进制数。写一个程序算出 和 的二进制结果数组。中间过程用到的所有变量取值只能为 ,常量不受限制。
问题1.2. 在上面的问题中,我们改为输入 个数组表示的二进制数,并且把中间过程中的每个变量改为 位二进制数,是否可以同时计算 组数的和?
问题2.1. 现在有 个非负整数( ),请你写一个程序算出:对于二进制的每一位,这些数中 的个数有多少。令 ()表示位数(下同),请你写出你算法的时间复杂度。( 远大于 )
问题2.2. 你能找到这个问题时间复杂度低于 的方法吗?
模拟 DFA
根据上面的问题,我们可以发现一个基本思路:如果我们能设计出一个所有中间变量取值均为 且没有条件判断的算法,显然就可以用位运算并行处理。
问题3. 你在做一个数据结构题,推导之后你发现,每一位信息可以写成一个有 个状态的 DFA。但是这个问题需要同时计算多个互相独立的位。现在你想用位运算加速。
问题3.1. 如何写一个程序模拟计算一个 DFA 的转移过程,且中间过程用到的所有变量取值都只能为 ?
问题3.2. [BZOJ2908部分] 给出一棵树,树上每个点都有点权,定义树上从 到 的费用为树上路径上的点的权值顺次 (与非)的结果。
本身不满足结合律,所以在这个数据结构中我们维护的路径信息是:对每一位分别维护“每种可能输入从左到右依次 这一段路径后得到的结果”
你能否用位运算同时处理所有位的信息,而不用枚举每一位?
练习
问题4. 给你 个非负整数( )的序列,求其所有子区间的按位异或和的和。答案 。( )
问题5. 给你 个非负整数( )的序列,求其所有子区间的按位或和的和。答案 。( )
答案
部分因为作者今天记录较为匆忙,没有代码,可以根据提纲来自己手写代码或者让AI生成。
问题0(只做了解即可)
__builtin_popcount 返回二进制中 的个数
__builtin_clz 返回二进制中前导 的个数
__builtin_ctz 返回二进制中尾缀 的个数
__builtin_parity 返回二进制中 的个数奇偶性
__builtin_ffs返回二进制中最低位 的位置
问题1.1&1.2——基本加法乘法模拟(高精度基础)
位和 位的差不多,最多改个数组大小,这里以 位自然溢出为例:
void add(bool a[64],bool b[64],bool(&ans)[64]){
bool f=0;
for(int i=63;~i;i--){
ans[i]=a[i]^b[i]^f;
f=a[i]&b[i]|a[i]&f|b[i]&f;
}
}
void mul(bool a[64],bool b[64],bool(&ans)[64]){
for(int i=63,k=0;~i;i--,k++){
bool f=0;
for(int j=63;~(j-k);j--){
bool tmp=ans[j-k]^(a[j]&b[i])^f;
f=a[j]&b[i]&ans[j-k]|a[j]&b[i]&f|ans[j-k]&f;
ans[j-k]=tmp;
}
}
}
只不过,64个数组并行加法的时候进位可能不只是一个布尔类型变量,而是一个长度约为 的布尔数组,但逻辑上类似
不过还有一个做法: 数组在二进制下,从低到高,每个数的第一位写为 ,每个数的第二位写为 ,依此类推;
数组在二进制下,从低到高,每个数第一位写为 ,第二位写为 ,依此类推。
这样,可以把 与 的元素逐一相加变为几个二进制数组相加。
当然,这里看似没有用,但是接下来这种想法会有助于我们完成问题2。
问题2.1
正常想到的暴力思路就是 的时间复杂度,暴力拆出每一位,然后进行统计
int ans[64]={};
for(int i=1;i<=n;i++){
int pos=63;
while(a[i]){
ans[pos]+=a[i]&1;
a[i]>>=1,pos--;
}
}
问题2.2
可以像刚才一样的过程,列出这 个数的二进制每一位,记作 ,这样,只需要像前面一样,横向处理会快捷很多。
当然,速度还可以更快,像启发式合并一样,原本像 一样的操作,现在改为 ,这样子,每次加法的时候进位会少很多,因而时间复杂度会降低。其时间复杂度为:
利用放缩可以得到,
因而,其时间复杂度渐进等价于 。
问题3.1——模拟DFA
DFA:一个程序拥有固定几种状态,每次输入一个新的字符,根据当前状态和新字符进行一个确定的转移的过程叫做DFA
设状态数位 ,直接开 个 变量,第 个表示在不在第 个节点,这样每个 取值可以和它下一个位置用位运算进行关联。
当然,读入字符也全变为 向量。
由于处理较小的自动机,因而我们模拟过程中代价也是常数。相当于在正确的位置时 ,其它都是 ,从存确切数字变为存储向量
问题3.2——
原题:书上路径,处理方式:树链剖分
题意:查询区间从左往右 的结果。
输入为 ,进入 函数时输出只可能是 ,因而可以利用转移到 的系数利用矩阵乘法进行优化。
例如,本来矩阵乘法为
现在,只需要把它变为 \displaystyle C_{i,j}=\or_{1\leq k\leq n}A_{i,k}\and B_{k,j}
问题4
前缀异或和,问题转化为 ,利用问题2的方法优化时间复杂度
问题5
某一位按位或和为 的区间,只可能是这一位全为 的区间,因而只需要对于每一位分别统计这一位上每一段连续 ,计算它们这一段长的平方和。
接下来还是利用问题2的思路,设区间长度为 ,当前位的后一位为 ,取 ,然后让 ,并让 的每一位 \and(!c),这样只有 次操作,
不过,时间还会爆炸,因而,需要用到前面所说的启发式合并式操作,维护区间端点是 ,然后合并。
总之,它必然可以转换成纯粹的位运算。
记上一区间首位为 ,值为 ,下一区间首位为 ,值为 ,可以理解为贡献为 (R_1\times L_2)\and e_1\and s_2
这里唯一的难点就是乘法,不过乘法已经在问题1中解决,因而时间复杂度只有 ,又因为位数只是段长,所以时间复杂度为
总结 - OI卡常优化:
将 if 判断语句改为固定电路,一定有这样满足要求的手法能够进行这样的计算。本来是数组,现在是存储其每个数的第 位,并行这么多数的计算,在某些问题中能够起到优化作用
名师建议:一般只用来卡常优化,需要在复杂度除以 的时候用。比如你发现时间复杂度奔着线性去的时候去用。(当然,你故意乱搞的情况下除外。)
在NOIP阶段举出一个例题较为困难,因此只做了解即可。
声明
这个是某位著名OI老师的思考题,作者只是作为转载,可能有记录错误,还请见谅。
这里空空如也














有帮助,赞一个