位运算_1 2016.6.3

来源:互联网 发布:python编程控制机器人 编辑:程序博客网 时间:2024/06/11 13:48

参考:

http://www.matrix67.com/blog/archives/263

http://www.matrix67.com/blog/archives/264

http://www.matrix67.com/blog/archives/266

http://www.matrix67.com/blog/archives/268

%%%Matrix67


由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快


1、位运算符号

a & b 位与

a | b 位或

a ^ b 位异或

  ~a 位取反

a << b 位左移

a >> b 位右移


2、位运算的使用

(1)& 运算

位与运算主要有2个功能

①屏蔽

例:若 a 为一个 32 位的无符号型整数,表达式 a & 0xfffffffc 的结果可以使 a 的机器码的第 0、1 位为 0

②测0

可以用来判断一个非 0 的整数 N 的奇偶性,它的二进制的最末位为0表示该数为偶数,最末位为1表示该数为奇数

if (N & 1) {    printf("N is an odd\n");} else {    printf("N is an even\n");}

(2)| 运算

位或运算的主要功能是置 1

例如一个数 | 1的结果就是把它的二进制最末位变成1

如果需要把二进制最末位变成0,对这个数 | 1之后再减一就可以了,其实际意义就是把这个数变成最接近的偶数


(3)^ 运算

①位求反

因为异或可以这样定义:0 和 1 异或 0 都不变,异或 1 则取反

②相等比较

若 a ^ b 的值为 0 则 a 和 b 相等,否则不相等


所以两次异或同一个数最后结果不变,即(a ^ b) ^ b = a

位异或运算可以用于简单的加密

比如我想对我 MM 说 1314520,但怕别人知道,于是双方约定拿我的生日 19960112  作为密钥

1314520 xor 19960112 = 19177448,我就把 19177448 告诉 MM

MM 再次计算 19177448 xor 19960112 的值,得到 1314520,于是她就明白了我的企图


③交换两个变量的值

可以使用 a = a ^ b,b = a ^ b,a = b ^ a来实现

相当于 b = (a ^ b) ^ b = a,a = a ^ (a ^ b) = 0 ^ b = b


等效于 a = a + b,b = a - b ,a = a - b

相当于 b = (a + b) - b = a,a = (a + b) - a = b


(4)~运算

位取反运算的含义是对参与运算的运算对象的机器码按二进制方式对相应位进行位取反,1 变为 0,0 变为 1


使用not运算时要格外小心,你需要注意整数类型有没有符号

如果位取反的对象是无符号整,那么得到的值就是它与该类型上界的差

例:

typedef unsigned int uint;uint N = 100;N = ~N;printf("%u\n", N);//Result : 4294967195

位取反运算的主要功能是测试某些位是否为 1


例:测试某整型变量 a 的第 0、4、8、12 位是否为 1

可以首先对 a 进行取反,再对取反后的值测 0

表达式 ~a & 0x1111 的值为 0,则表示被测的 0、4、8、12 位为 1,否则被测的位不全部为 1


(5)<< 运算

位左移运算符构成的表达式一般格式为 a << n ,其中 a 是需要移位的数据, n 是移位的位数

左移后的结果相当于 a * 2 ^ n

但此结论只适合没有溢出的情况

对于正整数而言,移出的位没有出现 1,且移位后的最高位为 0,则不溢出

对于负整数而言,移出的位没有出现 0,且移位后的最高位为 1,则不溢出


可以用位左移运算来定义 maxn 等常量


(6)>> 运算

位右移运算符构成的表达式一般格式为 a >> n ,其中 a 是需要移位的数据, n 是移位的位数

右移后的结果相当于乘以 a * 2 ^ (-n)

但此结论只适合没有溢出的情况


我们经常用 >> 1来代替 / 2,比如二分查找、堆的插入操作等等

想办法用位右移运算代替除法运算可以使程序效率大大提高

最大公约数的二进制算法用除以 2 操作来代替慢得出奇的 mod 运算,效率可以提高60%



Matrix67大牛博客的那个例题的链接失效了

我在这里放个传送门

Vijos 1197 费解的开关 (Matrix67原创)

但是我不懂那些A题的姿势,等看完大牛的博客,学习了新姿势以后再来做吧,QAQ


贴一个我最近A的一道和位运算能扯上关系的题

CodeForces_677C Vanya and Label

#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <algorithm>#include <queue>#include <vector>#include <stack>#include <map>#include <cmath>using namespace std;typedef long long ll;typedef unsigned long long ull;typedef unsigned int uint;const int mod = 1e9 + 7;const int INF = 0x7fffffff;const int maxn = 1e5 + 10;int num[300];char s[maxn];int _0_num[70];int Count_0(int n);int main(){#ifdef __AiR_H    freopen("in.txt", "r", stdin);#endif // __AiR_H    for (int i = '0'; i <= '9'; ++i) {        num[i] = i - '0';    }    for (int i = 'A'; i <= 'Z'; ++i) {        num[i] = i - 'A' + 10;    }    for (int i = 'a'; i <= 'z'; ++i) {        num[i] = i - 'a' + 36;    }    num['-'] = 62;    num['_'] = 63;    for (int i = 0; i < 64; ++i) {        _0_num[i] = Count_0(i);    }    gets(s);    int len = strlen(s);    ll ans = 1;    for (int i = 0; i < len; ++i) {        int t1 = num[(int)s[i]];        int t2 = _0_num[t1];        if (t2 != 1) {            ans = (ans * t2) % mod;        }    }    printf("%I64d\n", ans);    return 0;}int Count_0(int n){    int ret = 1;    int num = 6;    while (num--) {        if ((n&1) == 0) {            ret *= 3;        }        n >>= 1;    }    return ret;}


列举一些常见的二进制位的变换操作

⒈去掉最后一位               >> 1

⒉在最后加一个0            << 1

⒊在最后加一个1            (<< 1) + 1

⒋把最后一位变为1        | 1

⒌把最后一位变为0        (| 1) - 1
⒍最后一位取反              ^ 1

⒎右数第 k 位变为1       | (1 << (k - 1))

⒏右数第 k 位变为0       & (~(1 << (k - 1)))
⒐右数第 k 位取反         ^ (1 << (k - 1))

⒑取末三位                     & 7

⒒取末 k 位                     & ((1 << k) - 1)

⒓取右数第 k 位             (>> ( k - 1)) & 1

⒔把末 k 位变为1           | ((1 << k) - 1)

⒕末 k 位取反                 ^ ((1 << k) - 1)


⒖把右边连续的 1 变为 0              (100101111 -> 100100000)      x & (x + 1)

⒗把右边起第一个 0 变为 1          (100101111 -> 100111111)        x | (x + 1)

⒘把右边连续的 0 变为 1              (11011000 -> 11011111)            x | (x - 1)

⒙取右边连续的 1                          (100101111 -> 1111)                   (x ^ (x + 1)) >> 1

⒚去掉右起第一个 1 的左边         (100101000 -> 1000)                 x & (x ^ (x - 1))


最后一个在树状数组中会用到,还有一种写法是 x & (-x)


-x 实际上是 x 按位取反,末尾加 1 以后的结果

 388288 = 1001010110010000

-388288 = 0110101001110000



3、二进制位中的1有奇数个还是偶数个

int Judge(int n)    //二进制中 1 的个数的奇偶性,奇数返回 1,偶数返回 0{    int ret = 0;    while (n) {        if (n & 1) {            ret += 1;        }        n >>= 1;    }    if (ret & 1) {        return 1;    }    return 0;}

这样写的效率并不高,位运算的神奇之处还没有体现出来

下面这段代码就强了

int Judge(int n)    //二进制中 1 的个数的奇偶性,奇数返回 1,偶数返回 0{    n ^= (n >> 1);    n ^= (n >> 2);    n ^= (n >> 4);    n ^= (n >> 8);    n ^= (n >> 16);    if (n & 1) {        return 1;    }    return 0;}

为了说明上面这段代码的原理,我们还是拿1314520出来说事

1314520的二进制为101000000111011011000


第一次异或操作的结果如下:

          00000000000101000000111011011000
xor       0000000000010100000011101101100
—————————————————————
          00000000000111100000100110110100


得到的结果是一个新的二进制数,其中右起第 i 位上的数表示原数中第i和 i+1 位上有奇数个 1 还是偶数个 1

比如,最右边那个 0 表示原数末两位有偶数个 1,右起第 3 位上的 1 就表示原数的这个位置和前一个位置中有奇数个 1


对这个数进行第二次异或的结果如下:

        00000000000111100000100110110100
xor       000000000001111000001001101101
—————————————————————
        00000000000110011000101111011001

结果里的每个1表示原数的该位置及其前面三个位置中共有奇数个 1 ,每个 0 就表示原数对应的四个位置上共偶数个 1

一直做到第五次异或结束后,得到的二进制数的最末位就表示整个32位数里有多少个 1,这就是我们最终想要的答案



4、计算二进制中的 1 的个数


同样假设x是一个 32 位整数。经过下面五次赋值后,x 的值就是原数的二进制表示中数字 1 的个数

比如,初始时 x 为 1314520,那么最后x就变成了 9,它表示 1314520 的二进制中有 9 个 1


int Count_1(int x)    //计算二进制中 1 的个数{    x = (x & 0x55555555) + ((x >> 1) & 0x55555555);    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);    x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);    x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);    x = (x & 0x0000FFFF) + ((x >> 16) & 0x0000FFFF);    return x;}

为了便于解说,我们下面仅说明这个程序是如何对一个 8 位整数进行处理的

数字211的二进制为11010011


+—+—+—+—+—+—+—+—+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <—原数
+—+—+—+—+—+—+—+—+
|  1 0  |  0 1  |  0 0  |  1 0  |   <—第一次运算后
+——-+——-+——-+——-+
|    0 0 1 1    |    0 0 1 0    |   <—第二次运算后
+—————+—————+
|        0 0 0 0 0 1 0 1        |   <—第三次运算后,得数为5
+——————————-+


整个程序是一个分治的思想

第一次我们把每相邻的两位加起来,得到每两位里1的个数,比如前两位10就表示原数的前两位有2个1

第二次我们继续两两相加,10+01=11,00+10=10,得到的结果是00110010,它表示原数前4位有3个1,末4位有2个1

最后一次我们把0011和0010加起来,得到的就是整个二进制中1的个数


程序中巧妙地使用取位和右移

比如第二行中 0x33333333的二进制为 00110011001100….,用它和 x 做位与运算就相当于以 2 为单位间隔取数

位右移的作用就是让加法运算的相同数位对齐


5、二分查找32位整数的前导0个数

int Count_Pre0(unsigned int x)    //二分查找 32 位整数的前导 0 的个数{    if (x == 0) {        return 32;    }    int n = 1;    if ((x >> 16) == 0) {        n += 16; x <<= 16;    }    if ((x >> 24) == 0) {        n += 8; x <<= 8;    }    if ((x >> 28) == 0) {        n += 4; x <<= 4;    }    if ((x >> 30) == 0) {        n += 2; x <<= 2;    }    n -= (x >> 31);    return n;}


Vijos 1201 高低位交换(Matrix67原创)


其实当时搞单片机对这些位运算用的比较多

专门来研究的话更是感觉位运算厉害啊,虽然用位运算有时候会导致程序可读性很低,但是其实可以加个注释来说明一下就更好了


#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <algorithm>#include <queue>#include <vector>#include <stack>#include <map>#include <cmath>using namespace std;typedef unsigned int uint;int main(){    uint n;    scanf("%u", &n);    n = (n << 16) | (n >> 16);    printf("%u\n", n);    return 0;}




0 0
原创粉丝点击