chengni
2018-10-04 18:25:32
我们都知道,计算机是使用二进制进行存储和计算的。十进制的出现是因为人类有十个手指,比较方便表示十进制;而对于计算机而言,因为计算机要依靠电源,而电源只有通电和断电两种情况,所以二进制就成为了计算机的基础进制。
一道送分题
对于二进制,似乎更多的考题是在初赛,但复赛也会涉及到一些内容,而且也可以运用在生活中。
对于二进制数,首先要知道二进制是什么。类似于十进制,二进制也是一种进制(废话),但二进制运算遵循的规则是“进二”而不是我们熟悉的“进十”。
简单来说,二进制基数为
二进制转十进制
对于一个二进制数,他的十进制数为从左往右的每一位乘上该位数的
十进制转二进制
把上面的操作反过来,将十进制数除以
其他进制也可以用类似的方法进行转换,例题P1143 进制转换
原码,指一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1;二进制数等于0时,符号位可以为0或1。
反码:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。
补码:正数的补码与原码相同,负数的补码是其对应正数二进制所有位取反后加1。
在计算机中通常使用补码进行储存。
一道例题(2017tg初赛)
先确定该数是负数,然后还原操作得到
因为计算机使用的是二进制,自然就有独成一套体系的运算方式,即位运算。
位运算是计算机中最简洁的运算,并且具有很好用的性质,而且用起来还比较快。总而言之,位运算是十分实用的。
首先,在这里这东西跟
在二进制运算中,这东西叫做“左移”“右移”运算,顾名思义,就是将一个二进制数向左或向右移动
那么这东西有什么用呢?这东西快啊
又称取反运算,就是对一个二进制数按位取反。
对于
那么这东西有什么用呢?我也不知道
“&”运算,即“and” 运算,也是一种逻辑运算符,对于二进制运算来说,“&”运算的意义是对于两个二进制数的每一位,如果这一位都是
举个例子
10101(21) & 11100(28) = 10100{20}
我们可以用 & 运算判断一个数是奇数还是偶数,当
即 “or” 运算,也是一种逻辑运算符,对于二进制运算来说,“|” 运算的意义是对于两个二进制数的每一位,如果这两个数此位有一个
举个例子
10101(21) | 11100(28) = 11101(29)
通过对这两个运算的观察,我们可以发现一个规律
根据二进制的性质很容易就可以得出这些结论吧
“^”运算,又称“xor”运算,异或运算。定义是对于两个二进制数的每一位,如果相同则为
举个例子
10101(21) ^ 11100(28) = 1001(9)
异或是一个非常神奇的东东
首先显而易见的是一个数异或他自己肯定是得
其次对于一个形如
然后异或运算满足以下交换律
如果
异或运算还是比较常用到的,简单举两个例子
例题一
给你
原题 P1469 找筷子
利用异或的性质
例题二
计算
原题 P3908 异或之和
首先最开始是
然后得到
于是我们又回到了
接下来厉害的来了,这三种运算是可以互相转换的
但这东西似乎除了能让你感受到位运算的博大精深以外似乎什么用都没有
一道例题(2007tg初赛)
我猜很多人(其实是我)在做这道题的时候果断选了
你可能会懵逼了(其实还是我):我没算错啊。然后重新算一遍,还是
这是因为运算的优先级,本题中会先计算异或运算再计算或运算,也就变成了
关于位运算的优先级,大致按下面排序
加减运算
如果代码里不注意这些细节可能就会出错
比如对于
对于拿不准的地方还是加个括号为好
快速交换两个数
这东西理论上能比正常的交换优一点,当然还是用
lowbit运算
我们怎样能够快速求出一个二进制数位数最低的
根据取反运算的性质,
所以我们可以得出
主要的应用就是树状数组了,这个我不说你们也懂
当然也可以用来计算二进制数中出现了多少个
快速判断奇偶性
这个上面说了,不再多提
在状压情况下的操作
先简单介绍一下状压吧。
简单来说就是使用二进制数来表示某一种状态来减少储存的空间,比较常用的有状压 DP 和状压搜索。在洛谷上可以找到例题。
而状压比较常用的操作有(这个图是同学在网上找的,似乎有点小问题?优先级的话大家理性理解吧)
更多奇奇怪怪的应用可视题目要求所定。另外别的进制也可以用来做状态压缩,但是运算起来就没那么方便了,也视情况所定。
快速幂
快速幂顾名思义,就是快速算某个数的多少次幂。
求
大致过程是把
举个例子 当
大致代码如下
int ksm(int a,int b){
int ans=1;
while(b){
if(b&1){
ans=ans*a;
}
b>>=1;
a=a*a;
}
return ans;
}
小计算加速
前面说过了位运算的速度是比四则运算快的,所以可以用来稍稍的加一点速,比如对于整数二分的过程
我们就可以写成
或者在使用线段树的时候
我们可以写成
你要相信,这种写法真的不是在嘚瑟,这样真的会快那么一点的,说不定这么一点常数就能让你卡过一道题
bitset 是 C++ 里的一个类,我们可以简单认为它就是一个
bitset 在定义的时候一定要定义大小,特殊的是需要使用尖括号,如下。
bitset<1000>
举一个例子 P2347 砝码承重
这道题似乎是一个 DP 题,但巧妙的应用 bitset 的性质也可以完成。
因为 bitset 只能存
bitset能对某一位赋值,我们初始定义
当我们加入一个新的砝码的时候,我们将这个砝码加入 bitset ,即进行如下的操作
简单理解就是已有的所有能表示出的值都加上
最后我们统计 bitset 中有多少个
bitset 还有一些自带函数,例如
bitset 的复杂度:因为bitset是用 uint/long long 类型来实现每次同时操作 32/64 个 01 变量的东西,所以他的很多操作都是 O(1) 或 O(n/32) / O(n/64) 的。至于是 32 还是 64 ,一般看机器是32位还是64位
GCC提供了一系列的builtin函数,可以实现一些简单快捷的功能来方便程序编写,简单介绍几种常用的。
有些函数会直接把
int __builtin_ctz(x) //返回 x 二进制表示下后导 0 的个数
//带 0 的话会出32
int __builtin_clz(x) //返回 x 二进制表示下前导 0 的个数
//因为转成了 unsigned 带 1 的话是 31,带 2147483648 得 0,不过带 0 还是得 31
int __builtin_ffs(x) //返回二进制下最后一个 1 的位置。
//一般情况下就是 ctz+1
int __builtin_popcount(x) //返回 x 二进制表示下有多少位是 1
int __builtin_parity(x) //返回 x 二进制下 1 的个数的奇偶性。
以上函数的
例如
__builtin_ctzll(x)
这些函数是 gcc 自带的,不需要额外头文件。
二进制和位运算都属于比较简单的问题,经常活跃在初赛上,而对于复赛则比较基础了,我们更多的是利用二进制的一些性质优化复杂度和完成代码。
最后分享一个关于状态压缩的问题,来自于编程之美。
此时象棋棋盘上只剩下双方的将帅,我们知道将和帅是不能面对对方的,请用一个变量表示所有合法的状况。
我们可以使用一个长度为
有没有更简单的办法呢?
给九宫格的每一个位置一个标号,可以用两位的十进制数表示将和帅的位置。
你还有别的办法吗?