c++之位运算(详解,初学者绝对能看懂)

目录

一  位运算符号

移位运算:

二  常用技巧:

三  运算符号优先级:

四    位运算常用技巧

1   判断奇偶性

2  求a的b次方

3  找处未重复的数

4  用O(1)时间检测整数n是否是2的幂次.

5  计算在一个 32 位的整数的二进制表示中有多少个 1

6 快速幂

7 二进制状态压缩

8 二进制优化递归

9 一道经典题


一  位运算符号

&
按位与
如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
|
按位或
两个相应的二进制位中只要有一个为1,该位的结果值为1
^
按位异或
若参加运算的两个二进制位值相同则为0,否则为1
~
取反
~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1

举例

     1000101             1000101         1000101         1000101

&   0101100           |  0101110      ~                     ^ 0101110

=   0000100           = 1101111       = 0111010         1101011

移位运算:

<<
左移
用来将一个数的各二进制位全部左移n位,低位以0补充,高位越界后舍弃。

        1左移n位:                    1 << n=2^n(这里指2的n次方)

         n左移1位:                    n << 1=2*n

举例

       short a=9115         0010001110011011   (9115的二进制表示)

       a << 1 =18230       0100011100110110   (注意高位越界后舍弃一个0,低位填充一个0)

       a << 2  = -29076    1000111001101100   (注意高位越界后舍弃两个0,低位填充两个0)

注意:左移是做乘2的运算,但这是在符号位(原码将最高位符号以0表示正,1表示负eg:0010001110011011中的最高位0就是符号位,表示是整数;而1000111001101100最高位是1,表示是负数)不变的情况下。如果符号位发生了改变,说明已经不能做乘2的运算了,否则会溢出,得到的值不是乘2的结果。

注意:short是Java中的表示,它定义的也是整形,不过是16字节(这里为了阐述符号位的改变而使用),而int是32字节

>>
右移
将一个数的各二进制位右移N位,移到右端的低位被舍弃,高位以符号位填充

        n左移1位 :                     n >> 1=|n/2.0| 

        算术右移等于除以2向下取整,(-3)>> 1 = -2       ,3 >> 1 = 1

        值得一提的是,“整数/2”在c++中实现为“除以2向零取整”,(-3)/  2  =  -1,3 / 2 = 1

举例

short b = 9115                  0010001110011011 

(9115 >> 1) = 4557          0001000111001101(注意低位越界后舍去了一个1,高位补一0) 

(9115 >> 2) = 2278          0000100011100110  (注意低位越界后舍去了两个1,高位补两0)

short c=-32766(负数符号位为1) 1000000000000010

-32766 >> 1 = -16383                 1100000000000001(注意低位越界后舍弃一个0,高位补1)

-32766 >> 2 = -8192                   1110000000000000(注意低位越界后舍弃0和1,高位补俩1)

二  常用技巧:

 在m位二进制数中,通常称最低为为第0位,从右到左依次类推,最高位为第m-1位

(n >> k) & 1   求n二进制下的第k位是0还是1,若结果为真,是1,若结果为假,是0。因为1的二进制数中只有第0位数是1,其余位数都是0。

n^=1,,即n=n^1,能让n变成与原来相反的数(0或1)

n | (1 << k),能把n的第k位变成1

a^b^b=a

x=x&(x-1)用于消去x的最后一位

三  运算符号优先级:

加减优先级最高,位或优先级最低,从左往右优先级递减

加减    移位      比较大小      位与     异或        位或

+,-    <<,>>    >,<,==,!=        &          ^               |

四    位运算常用技巧

1   判断奇偶性

如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,代码如下:

if(n&1){    //若n&1==1(为真)
   //n是奇数
}
if(!(n&1)){ //若n&1!=1(为假)
  //n是偶数
}

2  求a的b次方

本题涉及数论数论数论,在次不详细解释,有兴趣的话可以自己了解.

int pow(int a,int b){
    int ans = 1;
    while(b != 0){
        if(b & 1 == 1){
            ans *= a;
        }
        a *= a;
        b = b >> 1;
    }
    
    return ans;
}

注意,若数据过大,应改为long  long

举例: b = 13,则 b 的二进制表示为 1101, 那么 a 的 13 次方可以拆解为:

a^1101 = a^0001 * a^0100 * a^1000。

我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。

这样时间复杂度是O(logn),比库里的pow函数O(n)快

3  找处未重复的数

数组中,只有一个数出现奇数次,剩下都出现偶数次,找出出现奇数次的。

思路:两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下。也就是说,那些出现了偶数次的数异或之后会变成0,那个出现奇数次的数,和 0 异或之后就等于它本身。

#include<iostream>
using namespace std;
int main(){
    int a[9]={4,3,2,2,2,2,5,4,3};
    int ans=0;
    for(int i=0;i<9;i++){
        ans^=a[i];
    }
    cout<<ans;
    return 0;
}

4  用O(1)时间检测整数n是否是2的幂次.

思路:

  • n如果是2的幂次,则:
    1.n>0
    2.n的二进制表示中只有一个1
    一位n的二进制表示中只有一个1,所以使用n&(n-1)将唯一的一个1消去。
    如果n是2的幂次,那么n&(n-1)得到结果为0,即可判断。
  • eg: 8的二进制位1000,7的二进制位0111,8&7==0,所以8是2的幂次
  • #include<iostream>
    using namespace std;
    int main(){
        int n;
        cin>>n;
        if(!(n&(n-1))) cout<<"YES";
        if(n&(n-1)) cout<<"NO";
        return 0;
    }
    

5  计算在一个 32 位的整数的二进制表示中有多少个 1

思路:

由 x & (x-1) 消去x最后一位知。循环使用x & (x-1)消去最后一位1,计算总共消去了多少次即可

#include<iostream>
using namespace std;
int get(int num) {
    // write your code here
    int count =0;
    while(num){
        count ++;
        num = num&(num-1);
        cout<<num<<endl;
    }
    return count;
}
int main(){
    int n;
    cin>>n;
    cout<<get(n);
    return 0;
}

6 快速幂

int ksm(int a,int b)//快速幂函数
{
    int ans=1;
    a%=Mod;
    while(b)
    {
        if (b&1)
            ans=ans%Mod*a;
        a=a%Mod*a%Mod;
        b>>=1;
    }
    return ans;
}

7 二进制状态压缩

       二进制状态压缩是指讲一个长度为m的bool数组用一个m位二进制整数表示并存储的方法。利用下列位运算操作可以实现bool数组对应下标元素的存取。

取出整数n在二进制表示下的第k位                              ( n >> k ) & 1

取出整数n在二进制表示下的第0~k-1位(后k位)      n & ( ( 1 << k ) - 1 )

把整数n在二进制表示下的第k位取反                          n ^  (1 << k)

对整数n在二进制表示下的第k位赋值1                        n | ( 1 << k )

对整数n在二进制表示下的第k位赋值0                        n & ( ~ ( 1 << k ) )

      最短Hamilton路径

给定一张 nn 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 n。

接下来 n行每行 n个整数,其中第 i 行第 j个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x]并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

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位二进制数,若其第i位(0<=i<n)为1,则表示第i个点已经被经过,反之未被经过。在任意时刻还需要知道当前所处的位置,因此我们可以使用f[i[[j](0<=i<2^n,0<=j<n)表示“点被经过的状态”对应的二进制数为i,且目前处于点j时的最短路径。总的时间复杂度是 O(n^2*2n)

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=20,M=1<<N;//2的20次方,数据最坏n是20,
//一个点都有用和不用(1和0)两种情况,n=20时就是2^20
int f[M][N],w[N][N];//w表示的是无权图
//状态:1.哪些点被用过  2.目前停在哪些点上   所以用二维
//eg:点0 1 4   M=10011(第0个点存在,第1个点存在,第2,3个点不存在,第4个点存在)
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;//因为零是起点,所以f[1][0]=0;

    for(int i=0;i<1<<n;i++)//i表示所有的情况,i表示的集合,哪些点是否被用
     for(int j=0;j<n;j++)//j表示走到哪一个点
      if(i>>j&1)//i的第j位(二进制)是1
       for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
        if(i>>k&1)//第k位有点存在
          //f[state][j]=f[state_k][k]+w[k][j]; 
          //state_k表示state除去j后的集合,k表示停在第k个点上,state_k要包含k
         f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
         //更新最短距离,枚举state_k这个集合里面的所有点

    cout<<f[(1<<n)-1][n-1]<<endl;//表示所有点都走过了,且终点是n-1的最短距离
    //位运算的优先级低于'+'-'所以有必要的情况下要打括号
    return 0;
}


8 二进制优化递归

从 1∼n这 n 个整数中随机选取任意多个,输出所有可能的选择方案。

输入格式

输入一个整数 n。

输出格式

每行输出一种方案。

同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。

对于没有选任何数的方案,输出空行。

本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。

数据范围

1≤n≤15

输入样例:

3

输出样例:


3
2
2 3
1
1 3
1 2
1 2 3

用一个二进制数表示选了哪些数,替代之前的a[20]数组
其中 state |= 1 << (i - 1) 代表状态的改变,选了i这个数
state ^= 1 << (i - 1) 代表状态的还原,还原没选i这个数的状态

#include <iostream>
using namespace std;

int n;
bool vis[20];

void dfs(int pos, int start, int tar, int state) {
    if (pos == tar + 1) {
        for (int j = 0; j < n; j ++ ) {
            if (state >> j & 1) cout << j + 1 << " ";
        }
        cout << endl;
        return ;
    }

    for (int i = start; i <= n; i ++) {
        if (!vis[i]) {
            vis[i] = true; state |= 1 << (i - 1);
                dfs (pos + 1, i + 1, tar, state);
            vis[i] = false;state ^= 1 << (i - 1);
        }
    }
}

int main() {
    cout << endl;
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        dfs(1, 1, i, 0);
    return 0;
}

当然,这道题也可以用二进制状态压缩来写

思路:题目要求的结果是2^n个
这2^n个选择情况,对应于一个n位的2进制数的各个位取0或取1的情况。
例中n=3,即
000 -> \n
001 -> 1
010 -> 2
100 -> 3
011 -> 1 2
101 -> 1 3
110 -> 2 3
111 -> 1 2 3

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    // state 是每一个状态
    for (int state = 0; state < 1 << n; state ++ ) {
        // 用指针j遍历二进制数state中的每一位
        for (int j = 0; j < n; j ++ ) {
            if (state >> j & 1) cout << j + 1 << " ";
        }
        cout << endl;
    }
    return 0;
}

9  一道经典题

起床困难综合症:

21 世纪,许多人得了一种奇怪的病:起床困难综合症,其临床表现为:起床难,起床后精神不佳。

作为一名青春阳光好少年,atm 一直坚持与起床困难综合症作斗争。

通过研究相关文献,他找到了该病的发病原因: 在深邃的太平洋海底中,出现了一条名为 drd 的巨龙,它掌握着睡眠之精髓,能随意延长大家的睡眠时间。

正是由于 drd 的活动,起床困难综合症愈演愈烈, 以惊人的速度在世界上传播。

为了彻底消灭这种病,atm 决定前往海底,消灭这条恶龙。

历经千辛万苦,atm 终于来到了 drd 所在的地方,准备与其展开艰苦卓绝的战斗。

drd 有着十分特殊的技能,他的防御战线能够使用一定的运算来改变他受到的伤害。

具体说来,drd 的防御战线由 n扇防御门组成。

每扇防御门包括一个运算 op 和一个参数 t,其中运算一定是 OR,XOR,AND 中的一种,参数则一定为非负整数。

如果还未通过防御门时攻击力为 x,则其通过这扇防御门后攻击力将变为 x op t。

最终 drd 受到的伤害为对方初始攻击力 x 依次经过所有 n 扇防御门后转变得到的攻击力。

由于 atm 水平有限,他的初始攻击力只能为 0到 m 之间的一个整数(即他的初始攻击力只能在 0,1,…,m中任选,但在通过防御门之后的攻击力不受 m 的限制)。

为了节省体力,他希望通过选择合适的初始攻击力使得他的攻击能让 drd 受到最大的伤害,请你帮他计算一下,他的一次攻击最多能使 drd 受到多少伤害。

输入格式

第 1 行包含 2 个整数,依次为 n,m,表示 drd 有 n 扇防御门,atm 的初始攻击力为 0 到 m 之间的整数。

接下来 n 行,依次表示每一扇防御门。每行包括一个字符串 op 和一个非负整数 t,两者由一个空格隔开,且 op 在前,t 在后,op 表示该防御门所对应的操作,t 表示对应的参数。

输出格式

输出一个整数,表示 atm 的一次攻击最多使 drd 受到多少伤害。

输入样例:

3 10
AND 5
OR 6
XOR 7

输出样例:

1

样例解释

atm可以选择的初始攻击力为 0,1,…,10

假设初始攻击力为 44,最终攻击力经过了如下计算

4 AND 5 = 4

4 OR 6 = 6

6 XOR 7 = 1

类似的,我们可以计算出初始攻击力为 1,3,5,7,9时最终攻击力为 0,初始攻击力为 0,2,4,6,8,10 时最终攻击力为 1,因此 atm 的一次攻击最多使 drd 受到的伤害值为 1。

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
const int N=1e5+10;
pair<string ,int> a[N];
int n,m;
int calc(int bit,int now){// 代表现在运算的是二进制的第几位, now有两种情况,需要运算过后返回
    for(int i=1;i<=n;i++){
        int x=a[i].second>>bit&1;
      //求参数的第k位是0还是1  首先取出第i次运算中的第几位进行二进制运算
        if(a[i].first=="AND") now=now&x;
        else if(a[i].first=="OR") now=now|x;
        else now=now^x;
    }
    return now;// 现在返回的就是运算过后的二进制位
}
/*本题思路
如果该位填 1 后,所得到的数大于 m,那么该位填 !u
(已经填好的更高位构成的数值加上1<<k  (k表示数的第k位),以后不超过m)
否则如果该位填 1 后,所得到的数对 n 个数都运算之后,
结果小于等于该位填 0 后得到的结果,那么为了让剩下能填的数更大,该位填 0
否则该位填 1
*/
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x;
        char str[5];
        scanf("%s%d",str,&x);
        a[i]=make_pair(str,x);
    }
    int val=0,ans=0;
    for(int bit=29;bit>=0;bit--){
   //因为本题中 m 最大是 10 ^ 9,log2(10 ^ 9) = 3log2(10 ^ 3) < 3 * 10 = 30
    //所以每次 i 从 29 往后枚举就可以了m最多有29个二进制位,直接从高位到低位枚举每一位
        int res0=calc(bit,0);
        int res1=calc(bit,1);
        if(val+(1<<bit)<=m&&res0<res1) // 满足填1的条件,直接填1
           val=val+(1<<bit),ans=ans+(res1<<bit);
        else// 不能填1,直接填0
           ans=ans+(res0<<bit);
    }
    cout<<ans<<endl;
    return 0;
}

参考《算法竞赛进阶指南》

  • 60
    点赞
  • 241
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 作为 C 知道,我可以给初学者一些建议,比如可以尝试编写一些简单的控制台程序,如计算器、猜数字游戏等。也可以尝试编写一些小型的工具,如文件管理器、文本编辑器等。此外,初学者还可以尝试参与一些开源项目,从中学习和提高自己的编程能力。总之,初学者应该从简单的项目开始,逐步提高自己的编程水平。 ### 回答2: 作为C语言初学者,可以尝试一些简单的项目来巩固基础知识和提升编程能力。 首先,一个常见的项目是学生信息管理系统。通过使用C语言的基本数据结构和文件操作,可以实现一个简单的学生信息管理系统,包括学生的姓名、学号、成绩等信息的录入、查询、修改和删除功能。 其次,可以尝试编写一个简单的计算器程序。利用C语言的基本数学运算和控制语句,可以实现一个支持基本四则运算的计算器,用户可以输入表达式并得到计算结果。 另外,可以尝试编写简单的游戏程序,比如猜数字游戏。通过使用C语言的随机数生成和条件语句,可以实现一个猜数字的小游戏,用户猜测一个随机生成的数字,程序会根据用户的猜测给出回答,直到猜对为止。 此外,还可以尝试编写一个简单的日历程序。利用C语言的时间和日期函数,可以实现一个显示当前日期和月份的日历,用户可以输入指定的年份和月份,程序会显示该月的日历。 总之,C语言初学者可以通过以上一些简单的项目来锻炼编程能力,熟悉C语言的各种基本语法和特性,为进一步的学习和项目开发打下基础。 ### 回答3: 对于C语言初学者来说,初学阶段可以尝试做一些简单的项目,以加深对C语言基础知识的理解和应用能力的培养。以下是一些建议的项目: 1. 简单的计算器:编写一个可以进行基本算术运算的计算器,可以接受用户输入并显示计算结果。 2. 学生成绩管理系统:编写一个简单的学生成绩管理系统,可以对学生的各科成绩进行录入、查询和统计分析。 3. 小游戏:编写一款简单的文字游戏,比如猜数字或者猜字谜,通过用户的输入与程序进行交互。 4. 图书管理系统:编写一个图书管理系统,可以进行图书的录入、查询和借阅管理等功能。 5. 简单的文件操作:学习并实践文件的读写操作,可以尝试写一个简单的文件备份程序或者文本编辑器。 以上项目只是一些初学者可以尝试的简单项目,通过这些项目的实践,可以更深入地理解C语言的基础语法和常见操作,培养自己的编程思维和解决问题的能力。随着不断的学习和掌握,可以逐渐挑战更复杂的项目,拓展自己的编程技能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值