进制学习指南
前置芝士
集合
其中 &表示按位与,∣ 表示按位或,⊕表示按位异或,∼ 表示按位取反。
[集合与集合]
[集合与元素]
通常会用到移位运算。其中 <<表示左移,>>表示右移。左移 i 位相当于乘 \(2^i\),右移 i 位相当于除 \(2^i\)。
[证明]
删除最小元素
s = 101100
s-1 = 101011 // 最低位的 1 变成 0,同时 1 右边的 0 都取反,变成 1
s&(s-1) = 101000
二进制运算技巧
-
利用异或操作 ^ 和空格进行英文字符大小写互换
('d' ^ ' ') = 'D' ('D' ^ ' ') = 'd'
-
消除数字 n 的二进制表示中的最后一个 1
n&(n-1)
-
判断两个数是否异号
int x = -1, y = 2; bool f = ((x ^ y) < 0); // true int x = 3, y = 2; bool f = ((x ^ y) < 0); // false
-
判断一个数是不是 2 的指数
bool isPowerOfTwo(int n) { if (n <= 0) return false; return (n & (n - 1)) == 0; }
-
不进位加法
a^b
-
进位加法
a + b = (a ^ b) + ((a & b) << 1) public int plus(int a, int b) { if (b == 0) return a; int _a = (a ^ b); int _b = ((a & b) << 1); return plus(_a, _b); }
7.数组中的数字只有一个出现了一次,其他的都出现了两次,找出这个只出现一次的数字。
public int singleNumber(int[] nums) { int result = 0, n = nums.length; for (int i = 0; i < n; i++) { result ^= nums[i]; } return result; }
-
取x最后一位1的位置
x&(-x)
-
去掉x的最后一位1
x&(-x)
-
把二进制的某一位置1或者置0
a | (1 << n)//将a的第n位置1 a & ~(1 << n)//把第n位置0
-
判断某一位是否为1
a & (1 << n)
-
补码
x&0xffffffff
-
//把两个32位数合并成一个64位数
long t = (ll) target[0] << 32 | target[1];
特别二进制表示
0xaaaaaaaa 表示'10101010101010101010101010101010'(32位)奇数位为1
0x55555555 表示'1010101010101010101010101010101'(31位)偶数位为1
一个数的倍数的二进制中的1出现次数的规律是与该数的二进制表示中1的个数有关
假设我们有一个数n,它的二进制表示中有m个1。那么n的倍数k的二进制表示中的1的个数可以通过以下规律计算:
- 如果k是n的倍数,那么k的二进制表示中的1的个数等于n的二进制表示中1的个数。
- 如果k不是n的倍数,那么k的二进制表示中的1的个数比n的二进制表示中1的个数多。
雷格码
n 位格雷码序列 是一个由 \(2^n\) 个整数组成的序列,其中:每个整数都在范围 \([0, 2^{n- 1}]\) 内。
- 第一个整数是
0
- 一个整数在序列中出现 不超过一次
- 每对 相邻 整数的二进制表示 恰好一位不同 ,且
- 第一个 和 最后一个 整数的二进制表示 恰好一位不同
[构造长度为n的雷格码]
G(i) = i ^ (i/2);
n = 3:
G(0) = 000,
G(1) = 1 ^ 0 = 001 ^ 000 = 001
G(2) = 2 ^ 1 = 010 ^ 001 = 011
G(3) = 3 ^ 1 = 011 ^ 001 = 010
G(4) = 4 ^ 2 = 100 ^ 010 = 110
G(5) = 5 ^ 2 = 101 ^ 010 = 111
G(6) = 6 ^ 3 = 110 ^ 011 = 101
G(7) = 7 ^ 3 = 111 ^ 011 = 100
【c++】
vector<int> grayCode(int n) {
vector<int> res;
for(int i=0;i<1<<n;i++){
res.push_back(i^i>>1);
}
return res;
}
集合操作全集
集合大小
//集合大小,即二进制表示中1的个数
int x=__builtin_popcount(2);//1
遍历集合
设元素范围从 0 到 n−1,从空集 ∅ 枚举到全集 U:
//c++
for (int s = 0; s < (1 << n); s++) {
// 处理 s 的逻辑
}
设集合为 s,从大到小枚举 s 的所有非空子集 sub:
//c++
for (int sub = s; sub; sub = (sub - 1) & s) {
// 处理 sub 的逻辑
}
[证明]
sub = (sub - 1) & s
为什么要写成 sub = (sub - 1) & s 呢?
暴力做法是从s 出发,不断减一直到0,但这样中途会遇到很多并不是s 的子集的情况。例如s=10101 时,减一得到10100,这是s 的子集。但再减一就得到10011 了,这并不是s 的子集,下一个子集应该是10001。
把所有的合法子集按顺序列出来,会发现我们做的相当于「压缩版」的二进制减法,例如
10101→10100→10001→10000→00101→⋯
如果忽略掉10101 中的两个0,数字的变化和二进制减法是一样的,即
111→110→101→100→011→⋯
如何快速找到下一个子集呢?以10100→10001 为例说明,普通的二进制减法会把最低位的1 变成0,同时1 右边的0 变成1,即10100→10011。「压缩版」的二进制减法也是类似的,把最低位的1 变成0,但同时对于1 右边的0,只保留在s=10101中的1,所以是10100→10001。怎么保留?& 10101 就行。
如果要从大到小枚举 s 的所有子集 sub(从 s 枚举到空集∅),可以这样写:
int sub = s;
do {
// 处理 sub 的逻辑
sub = (sub - 1) & s;
} while (sub != s);
当sub=0时(空集),再减一就得到-1,对应的二进制位111···1,再&x就得到了s,所以sub=s时说明最后一次循环的是空集,退出循环。
枚举所有大小恰好为k的子集
1)求出最低位的1开始的连续1的区间
2)将区间全部变为0,并将区间左侧的那个0变为1
3)将第1步取出的区间右移,直到剩下的1的个数减少了1个
4)将第2步和第三步的结果按位取或
举例:
k=4,comb=0101110->0110011
x=0100010,y=0110000
1)0101110->0001110
2)0101110->0110000
3)0001110->0000011
4)0110000||0000011->0110011
int comb=(1<<k)-1;//最小的子集
while(comb<1<<n){
//x为最低位的1,y:将cmob从最低位的1开始的连续的1都置0,并左一位加1.
int x=comb&-comb,y=comb+x;
//z=comb&-y:的得到了最低位1开始的连续区间,/x:将z右移,直到最低位为1,>>1:z的1的个数减少1个,
comb=((comb&-y)/x>>1)|y;
}
枚举不包含相邻元素的子集
进制转换
x :2-36的任何进制(int)(36是因为10个数字+26个英文字母,所以最多有36进制)
y :2-36的任何进制(int)
a :2-36的任何进制表示的数(string)
string divide(int x,string a,int y)
int charToInt(char x){
if(x<='9')return x-'0';
else return x-'A'+10;
}
char intToChar(int x){
if(x<=9)return x+'0';
else return x-10+'A';
}
string divide(int x,string a,int y){
string res;
while(a.size()!=0){
int remainder=0;
for(int i=0;i<a.size();i++){ // a/y一次
int num=remainder*x+charToInt(a[i]);
a[i]=intToChar(num/y);
remainder=num%y;
}
res+=(intToChar(remainder)); // res加上最后一位的余数
int pos=0;
while(a[pos]=='0')pos++;//因为字符串除法有前导0存在的可能,所以把‘0’去掉
a=a.substr(pos);
}
reverse(res.begin(),res.end()); //因为是相反的,随意最后进行反向。
return res;
}
void solve(){
int x,y;string a;
while(cin>>x>>a>>y){
for(int i=0;i<a.size();i++){
if(a[i]>='a'&&a[i]<='z')
a[i]=a[i]-'a'+'A';
}
cout<<divide(x,a,y)<<endl;
}
}
负二进制加法
arr1=[1,1,1,1,1] arr2=[1,0,1]
1)等位向加,如果x大于等于2,向前进位-1,因为在以负二进制为基地中,相邻位是一正一负。
2)如果carry=-1,但此位上都为0,则将此位上赋值为1,进位+1,例如\((2^0+2^0)=(2^2)-(2^1)\)。
3)最后将ans数组去除前导零,逆序输出,即为答案。
//就是普通的模拟进制,向前进位是-1,然后知道-1 == 11
vector<int> addNegabinary(vector<int>& arr1, vector<int>& arr2) {
int i = arr1.size() - 1, j = arr2.size() - 1;
int carry = 0;
vector<int> ans;
while (i >= 0 || j >= 0 || carry) {
int x = carry;
if (i >= 0) {
x += arr1[i];
}
if (j >= 0) {
x += arr2[j];
}
if (x >= 2) {
ans.push_back(x - 2);
carry = -1;
}
else if (x >= 0) {
ans.push_back(x);
carry = 0;
}
else {
ans.push_back(1);
carry = 1;
}
--i;
--j;
}
while (ans.size() > 1 && ans.back() == 0) {
ans.pop_back();
}
reverse(ans.begin(), ans.end());
return ans;
}
九进制特性
(1)移除所有包含数字9的数字,给我们一个数字n,让我们求在这个新的数组中第n个数字.
10,11,12,13,14,15,16,17,18 (移除了19,20.....80,81,82,83,84,85,86,87,88 (移除了89)......100
res:可以将十进制数n转为九进制数.
(2)移除所有包含数字k(1<=x<9)的数字,给我们一个数字n,让我们求在这个新的数组中第n个数字.
res:可以先将十进制n转为九进制数,让后观察每位数字,判断是否大于等于k,如果大于等于k,将这一位加1.