0x01 位运算

一些常用的类型的大概范围
4个字节的int类型大概在21亿左右(-2147483648~2147483647)即-2^31到(2^31)-1。
当任意两个数值做加减法运算,都等价于在32位补码下做高位不进位的二进制加减法运算,发生算术溢出时,32位无符号整数相当于对2^32取模,这也解释了有符号整数算术上溢时出现负数的现象。
0x3f3f3f3f是一个很有用的数值。它是满足以下两个条件的最大整数
1.整数的两倍不超过0x7f ff ff ff(0111……111),即int能表示的最大整数。
2.整数的每8位(每个字节)都是相同的。
memset(a,0x3f,sizeof(a))作用是将0x3f填充到数组a的每个字节上,而int有四个字节,所以用memset只能赋值出“每8位都相同”的int。
其实用 0x7f 7f 7f 7f也能初始化为最大值,不过我们需要把一个数组中的数值初始化成正无穷时,为了避免加法算术上溢或者繁琐的判断,我们经常用0x 3f 3f 3f 3f来初始化。
常见的取值范围
unsigned int 0~4294967295
int -2147483648~2147483647 (10位)
unsigned long 0~4294967295
long -2147483648~2147483647
long long的最大值:9223372036854775807 (19位)
long long的最小值:-9223372036854775808
unsigned long long的最大值:1844674407370955161
__int64的最大值:9223372036854775807
__int64的最小值:-9223372036854775808
unsigned __int64的最大值:18446744073709551615

移位运算
左移(低位以0填充,高位越界后舍弃)
例 1<<n = 2^n n<<1=2n
算术右移(高位以符号位填充,低位越界后舍弃)
注意:算术右移与整数/2的区别:算术右移等于除以2向下取整(-3>>1=-2)而整数/2等于相当于去掉小数部分(-3/2=-1)
逻辑右移(高位以0填充,低位越界后舍弃)
二进制状态压缩:是指将一个长度为m的bool数组用一个m位二进制整数表示并存储的方法。

操作运算
取出整数n在二进制表示下的第k位(n>>k)&1
取出整数n在二进制表示下的第0~k-1位(后K位)n&((1<<k)-1)
把整数n在二进制表示下的第K位取反nxor(1<<k)
对整数n在二进制表示下的第k位赋值1n or (1<<k)
对整数n在二进制表示下的第k位赋值0n&(~(1<<k))

异或的作用
1.成对变换:
通过计算可以发现,对于非负整数n:
当n为偶数时,n xor 1 等于n+1;
当n为奇数时,n xor 1 等于n-1;
这一结论常用于图论的邻接表中边集的存储。在具有无向边(双向边)的图中一对正反方向的边分别存储在邻接表数组的第n与n+1位置,(其中n为偶数)就可以通过异或来获得当前边(x,y)反向的边(y,x);
2.lowbit运算:定义为非负整数n在二进制数表示下“最低位的1及其后边所有的0”构成的数值,例:n=10(1010),lowbit(1010) = (10)
推导过程:设n>0,n的第K位是1,第0~k-1位都是0。
1.先把n取反,此时第K位变成0,第0~k-1位都是1,
2.再令n=n+1,此时因为进位,第K位变为1,第0~k-1位都是0.
所以n&(~n+1)仅有第k位为1,其余位为0,而在补码表示下。~n=-1-n,因此
lowbit(n) = n&(~n+1) = n&(-n);
例:1010-----0101------0110-----(1010)&(0110)=(0010)

下面的一个小技巧可以快速的知道二进制数中第几位是1。
思想:我们预处理一个数组H。令H[2^k] = k;如下所示:

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1<<20;
int h[N+1];
int main(){
	int n;
	for(int i=0;i<=20;i++) h[1<<i] = i;
	while(cin>>n){  //对多次询问进行求解
		while(n>0){
			cout<<h[n&-n]<<" ";
			n-=n&-n;
		}
	}
	cout<<endl;
	return 0;
}

稍微复杂但效率更高的方法是建立一个长度为37的数组H,令H[2^k mod 37] = k,这里利用了一个小的数学知识:存在0<=k<=35 2^k mod 37 互不相等,且恰好取遍1-36;

#include<iostream>
#include<algorithm>
using namespace std;
int h[37];
int main(){
    int n;
    for(int i=0;i<36;i++) h[(1ll<<i)%37] = i;
    while(cin>>n){
        while(n>0){
            cout<<h[(n&-n)%37]<<" ";
            n-=n&-n;
        }
        cout<<endl;
    }
    return 0;
}

题目一:给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2

方法一:这里直接使用x&1 得到最低位,然后判断最低位是否是1,然后再用移位运算符,再将最低位给抹掉。

#include<iostream>
#include<algorithm>
using namespace std;
int main(){
	int n;
	cin>>n;
	for(int i=0;i<n;i++){
		int x ,count;
		cin>>x;
		count = 0;
		while(x){
			if(x&1) count++;
			x=x>>1;
		}
		cout<<count<<" ";
	}
	return 0;
}

方法二:第二种方法使用了lowbit方法,每次循环将最低位的1至最后的0所组成的二进制 去掉,当数为0时,就是循环结束条件。

#include<iostream>
#include<algorithm>
using namespace std;
int main(){
	int n;
	cin>>n;
	while(n--){
		int x,count;
		cin>>x;
		count = 0;
		for(int i=x;i;i-=i&-i) count++;
		cout<<count<<" ";
	}
	return 0;
}

题目二:求 a 的 b 次方对 p 取模的值。

数据范围
0≤a,b,p≤10^9
数据保证 p≠0

输入样例:
3 2 7
输出样例:
2

题目思路:如果我们直接求a^b%p 这样肯定超过了限制的时间。我们可以这样想,任何一个整数都可以化成一个二进制数,我们先将b表示成这样的形式(假设b在二进制表示下有K位)
b=c(k-1)*2^(k-1) + c(k-2)*2^(k-2) + …+ c(0)*2^(0)
于是可以得到
a^b = a^(c(k-1)*2^(k-1)) * a^(c(k-2)*2^(k-2) ) *…* a^(c(0)*2^(0) )
例:b = 10 = (1010) ----- a^10 = a^(1010) = a^8 *a^2
这样就可以将o(n)的复杂度变成o(logn)的复杂度.

#include<iostream>
#include<algorithm>
using namespace std;
int main(){
	int a,b,p,sum;
	cin>>a>>b>>p;
	sum = 1%p; //当a,0,p时,当b=0时,
	while(b){
		//先将b的最低位提取出来,如果是1, 就将结束乘a^(b&1)
		if(b&1) sum = 1ll*sum*a%p;   //乘1ll是为了防止溢出int的范围.
		a = 1ll *a*a%p;  //最低位的a是a^1  次最低位 a 是a^2  再次最低位 a 是a^4  依次类推,所以累乘.
		b=b>>1;   //将最低位抹去
	}
	return 0;
}

题目二: 求 a 乘 b 对 p 取模的值。
数据范围
1≤a,b,p≤10^18

输入样例:
3
4
5
输出样例:
2

方法一:
题目思路:由于a,b,p都是10^18级别,而long long 最多可以表示19位十进制数,根本就不可能直接ab再求模,只能类似用快速幂的思想,b可以表示为:
b=c(k-1)*2^(k-1) + c(k-2)*2^(k-2) + …+ c(0)*2^(0)
然后a
b= a*(c(k-1)*2^(k-1)) + a*(c(k-2)*2^(k-2) ) +…+ a*(c(0)*2^(0) )

#include<iostream>
#include<algorithm>
using namespace std;
int main(){
	long long a,b,p,sum;
	cin>>a>>b>>p;
	sum = 0;
	while(b){
		if(b&1) sum = (sum+a)%p;
		a =a*2%p;  //a最大是18位十进制数,而long long最大可以表示19位十进制数,所以不会越界.
		b>>=1;
	}
	cout<<sum<<endl;
	return 0;
}

方法二:
题目思路:利用ab mod p = ab - ((下取整)ab/p)p;
首先,当a,b<p时,a
b/p下取整以后也一定小于p,所以我们可以用浮点数执行a
b/p的运算,而不用关心小数点之后的部分,正好是我们所需要的取整的部分。
另外,虽然ab和 ((下取整)ab/p)p可能很大,但是二者的差一定在0~p-1之间,所以我们只关心他们的较低位即可,所以我们用long long来存储ab和 ((下取整)a*b/p)*p各自结果。整数运算溢出相当于舍弃高位。正好也符合我们的要求。

#include<iostream>
#include<algorithm>
using namespace std;
int main(){
	long long a,b,p,sum;
	cin>>a>>b>>p;
	sum = 0;
	a = a%p;b = b%p;
	long long c = (long double) a*b/p;
	sum =a*b-c*p;
	if(sum<0) sum+=p;
	else if(sum>=p) sum-=p;
	cout<<sum<<endl; 
	return 0;
}

题目三: 最短Hamilton路径:给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
数据范围
1≤n≤20
0≤a[i,j]≤10^7

输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:
18

题目思路:
先来想一下暴力求解,就是枚举n个点的全排列,计算路径长度取最小值,时间复杂度o(n*n!),如果使用二进制状态压缩dp可以优化至o(n^2*2^n);我们可以使用一个n位二进制数,当第i位为1,表示这个点已经被经过,反之则没有经过。我们还可以使用F[i,j] (0<=i<2^n 0<=j<n) 表示“点被经过的状态”对应的二进制数,且目前处于点j时的最短路径。

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N =20;
const int M  = 1<<N;
int w[N][N];
int f[M][N];

int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>w[i][j];
    memset(f,0x3f,sizeof(f));
    f[1][0] =0;  //即只经过点0,目前处于起点0,最短路长度为0
    for(int i=1;i<1<<n;i++)  //n位二进制,有2^n种状态,一次一次遍历
        for(int j=0;j<n;j++)
            if(i>>j&1)  //当i的第j位为1, 即当前走到了j这个点。
                for(int k=0;k<n;k++)
                    if((i^1<<j)>>k&1)  //我们就去掉这个j这个点,然后遍历K,如果K为1,则说明可以从k点到j点,
                        f[i][j] = min(f[i][j],f[i^1<<j][k]+w[k][j]);  //判断是从K点到j点的距离小,还是直接到小,选择一个最小的值。
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

优化代码:上面的方法是,遍历状态,然后再通过一次一次循环来找到j和k,而我们可以看到,状态中包含了j和k,所以我们可以用位运算来提取这些有价值的j和k,不像上面的方法一样,进行循环,浪费时间。从漫无目的的循环到有目的提取,减少循环次数,提高效率。

#include <stdio.h>
#include <string.h>
const int N = 20;
const int M = 1 << N;
int n;
int w[N][N];
int f[M][N];
int log_2[M >> 1]; // 存 1 到 M / 2 中,所有 2 的整次幂的数以 2 为底的对数

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            scanf("%d", &w[i][j]);

    for (int i = 0; i < 20; i ++ ) // 预处理 log_2
        log_2[1 << i] = i;

    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    for (int state = 1; state < 1 << n; state ++ )
        if (state & 1)
            for (int t = state; t; t &= t - 1)     // 只枚举 state 所包含的点集
            {
                int j = log_2[t & -t];             // 将集合 t 中最小的点取出
                for (int u = state ^ t & -t; u; u &= u - 1) // 只枚举 state 中去掉点 j 的点集
                {
                    int k = log_2[u & -u];         // 将集合 u 中最小的点取出
                    if (f[state ^ t & -t][k] + w[k][j] < f[state][j])
                        f[state][j] = f[state ^ t & -t][k] + w[k][j];
                }
            }

    printf("%d\n", f[(1 << n) - 1][n - 1]);
    return 0;
}

一些关于移位运算符的优先级
加减(+ -) > 移位(<< >>) > 比较大小(> < == !=) > 位与(&) > 异或( xor ^) > 位或( | )

题目四: 起床困难综合症:给定 n,m 以及 n 个数和该数所对应的运算,其中运算有 与、或、异或 三种,问在所有不大于 m 的非负整数中,对给定的 n 个数都按该数所对应的运算运算一遍后,能得到得最大的值是多少。

输入样例:
3 10
AND 5
OR 6
XOR 7

输出样例:
1

题目思路:本题是让我们选择一个【0,m】之间的一个整数,然后给定的n次位运算,使结果最大。但是我们知道位运算的主要特点之一是在二进制表示下不进位。即参加位运算的各个位之间是独立无关的。所以我们可以从高位到低位,依次考虑每一位填0还是1。
x的第k位应该填1,当且仅当同时满足下列两个条件:
1.已经填好的更高位构成的数值加上1<<k以后不会超过m
2.用每个参数的第K位参与位运算,若初值为1,则n次位运算后结果为1;若初值为0,则n次位运算的结果为0。(如果该位填 1 后,所得到的数对 n 个数都运算之后,结果小于等于该位填 0 后得到的结果,那么为了让剩下能填的数更大,该位填 0)
如果不满足上述条件,要么填1会超过m的范围,要么填1不如填0更优。确定每一位后,就可以确定最后的结果。

#include<iostream>
#include<algorithm>
using namespace std;
const int N  =100010;
int n,m;
int t[N];  //用来存储输入的n个数
int op[N]; //用来存储输入的n个数的相应的运算符
char str[4];  //用来暂时存放输入的运算符
int calc(int bit,int now){
    for(int i=0;i<n;i++){
        int x = t[i]>>bit&1;  //题目输入n个数,将每个数右移bit位再与1,提取第bit位, 再让其与这个数的运算符进行操作。
        if(op[i] == 1) now|=x;
        if(op[i] == 2) now^=x;
        if(op[i] == 3) now&=x;
    }
    return now;  //返回结果。
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        cin>>str>>t[i];
        if(*str =='O') op[i] = 1;   //当输入OR时表示成1;当输入XOR时表示成2;当输入AND时表示成3。
        if(*str =='X') op[i] =2;
        if(*str =='A') op[i] =3;
    }
    int val =0;  //当第K位填1时,但是构成的结果不能超过m;val就是用来判断是否超过m
    int ans = 0;  //用来存储最大能表示的数值
    for(int bit=29;bit>=0;bit--){  //m 最大是 10 ^ 9,log2(10 ^ 9) = 3log2(10 ^ 3) < 3 * 10 = 30,所以每次 i 从 29 往后枚举就可以了
        int res0 = calc(bit,0);
        int res1 = calc(bit,1);
        if(val+(1<<bit)<=m && res0<res1)  //当第bit位填1不超过m时,而且填1比填0更优时,直接填1。反之填0
            val+=1<<bit,ans+=res1<<bit;
        else ans+=res0<<bit;
        }
    
    cout<<ans<<endl;
    return 0;
}

总结:位运算非常基础,涉及移位,二进制状态压缩,位的提取,lowbit运算等相关知识。快速幂算法通过将其中一个数变成二进制的表示形式,将o(n)变成o(logN)提高效率,然后就是二进制压缩的二道题目,一个dp中使用状态压缩,节省空间,还有一个起床困难的题目,体现了二进制的特点:二进制表示下不进位,即参与位运算的各个位之间是独立无关的。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值