巧妙算法之位运算的应用


 目录

位运算的常用几种方法

lowbit函数

二进制的相关概念 

位运算的符号

按位与&

按位或|

按位异或^

交换律结合律

按位取反

左移右移

扫描二维码关注公众号,回复: 14291826 查看本文章

规律记忆

知识巩固 

真题改编 


首先感谢2022.6.13杨超凡giegie带来的位运算算法讲解 

位运算的常用几种方法

lowbit函数

int lowbit(int x){return x&(-1);}

lowbit函数可以用于查找一个数字的二进制表示形式当中的最右边的1,lowbit函数返回的值为x的最右边1所代表的十进制整数。例如10的二进制形式是1010,lowbit(10)返回的二进制形式也就是10,转化为十进制也就是2。因此,lowbit函数常常用来解决求某个十进制整数当中含有多少个1的问题

具体应用

二进制当中1的个数

题目描述

给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。

输入格式

第一行包含整数n。第二行包含n个整数,表示整个数列。

输出格式

共一行,包含n个整数,其中的第i个数表示数列中的第i个数的二进制表示中1的个数。

数据范围

1<=n<=100000

0≤数列中元素的值≤10^9

输入样例

5
1 2 3 4 5 

输出样例

1 1 2 1 2

源代码

#include <iostream>
using namespace std;
const int N = 1000000+10;
int a[N];
int lowbit(int x)
{
	return x&(-x);
} 
int main()
{
	int n;
	cin >> n;
	for(int i = 1;i <= n;i ++ )cin >> a[i];
	for(int i = 1;i <= n;i ++ )
	{
		int ans = 0;
		while(a[i])
		{
			a[i] -= lowbit(a[i]);
			ans ++ ;
		}
		cout << ans <<' ';
	}
	return 0;
}

求取数的二进制的某一位的值与二进制枚举

求取n的二进制形式第k位的值:n>>k&1

例如10的二进制形式是1010

int a = n >> k & 1;a的值为0

int b = n >> k & 1;b的值为1

int c = n >> k & 1;c的值为0

int d = n >> k & 1;d的值为1

本方法常常与二进制枚举连用

用二进制枚举的情况往往都能够用DFS来解决,二者的共同特点都是暴力且有两个分支。在二进制枚举过程当中。用0和1来代表两种不同的情况,二进制枚举的模板如下所示

for(int i = 0;i < (1ll >> n);i ++ )
{
    for(int j = 0;j < n;j ++ )
    {
        int num = i >> j & 1;
        //分情况进行相应的处理
    }
}

具体应用 

凑数

题目描述

给定n个正整数,请判断能否用这n个数且每个数只能用一次,凑出来s
ps:建议使用二进制枚举

输入格式

第一行两个数n,s
第二行n个正整数

输出格式

如果能凑出输出YES,否则输出NO

样例输入

5 12
2 4 6 8 10

样例输出

YES

源代码 

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1000000+10;
int a[N];
int main()
{
	ll n,s,sum,flag;
	cin >> n >> s;
	for(int i = 0;i < n;i ++ )cin>>a[i];
	for(int i = 0;i < (1ll << n);i ++ )
	{
		sum = 0,flag = 0;
		for(int j = 0;j < n;j ++ )
		{
			int num = i >> j & 1;
			if (num == 1)sum += a[j];
		}
		if(sum == s)
		{
			flag = 1;	
			break;
		}
	}
	if(flag == 1)cout<<"YES";
	else if(flag == 0)cout<<"NO";
	return 0;
}

二进制的相关概念 

 只有0和1,逢二进一

二进制枚举的意义:利用二进制进行状态枚举,对于每一次的状态枚举0和1分别代表不同的状况

位运算的符号

按位与&

 1&1=1 1&0=0 0&1=0 0&0=0          &&逻辑且常用于条件判断

例如6&7 实际为 110 和 111 的运算,根据按位与的运算法则结果为 110 也就是6

口诀为全一则一

常常用来判断奇偶,n&1

任何数按位与运算1结果是其本身

按位或|

1|1=1 1|0=1 0|1=1 0|0=0      ||逻辑或常用于条件判断

例如6|7 实际为 110 和 111 的运算,根据按位或的运算法则结果为 111 也就是7

口诀为有一则一

任何数按位或运算0结果是其本身

按位异或^

1^1=0 1^0=1 0^1=1 0^0=0  

例如6^7 实际为 110 和 111 的运算,根据按位异或的运算法则结果为 001 也就是1

口诀为同零异一

任何数按位异或运算0结果是其本身

交换律结合律

按位与、按位或、按位异或遵守交换律与结合律

按位取反

按位取反可以求取一个数的负数减一,即~x=(-x-1)

左移右移

因c++运算符的重载,插入流也有着左移的功能,输出流也有着右移的功能

n>>k n除2的k次方   n<<k n乘2的k次方

规律记忆

同一个数对自身进行按位与和按位或都是其本身,对自身按位异或运算为0

a|(b&c) = (a|b)&(a|c)    a^(b&c) = (a^b)&(a^c)    a&(b^c) = (a&b)^(a&c)

根据交换率和结合律可自行继续推演,此处显示的公式仅仅为示例 

知识巩固 

麻烦的运算(按位&的规律题)

题目描述

小明刚刚学习了位运算中按位与的运算法则,想计算从n到m之间的数连续进行与运算之后的结果是多少,但是小明觉得一个一个的计算实在是太麻烦了,你能帮助他解决吗?

输入格式

两个整数n和m

输出格式

输出区间[n,m]之间的数进行与运算的结果

数据范围

0<=n<=m<=1e9

样例输入

11 13

样例输出

8

源代码  

当从L到R连续进行按位与运算的话会使时间超限,因此我们需要找规律进行优化,显而易见的是L是一定小于等于R的,根据十进制整数转换为二进制整数的规律来看,当L的二进制形式位数只会小于等于R的二进制形式位数。因此问题简化为三个分支进行讨论:

若L与R的二进制位数不相等时,证明从L到R一定发生了进位,一旦有进位情况的发生那么就意味着在R的二进制形式下匹配不到L的二进制的位数全为0,而能够匹配到的也因为从L到R之间的连续按位与运算也为0,因此此情况只有一个答案那就是0

若L与R的二进制位数相等时

当L完全与R相等,根据运算律可以得到一个数对自身按位与运算还是那个数本身,因此答案为L(R)

当L与R不相等时,从最高位查找关键点(A[i] != B[i]时的下标i),关键点也就是发生进位的点,因此后面的数字全部换为0,再将处理过后的二进制数转化为十进制即为最终答案

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
int main()
{
    //数据记得开long long,因最后一个测试点是十位数 
    ll l,r;
    cin>>l>>r;
    //如果二者相等那么等于其本身 
    if(l == r)
    {
        cout<<l;//或者cout<<r; 
        return 0;
    }
    //A,B用于装l,r转化为二进制之后的字符串 
    vector<int> A,B;
    //转换方法为不断对2取余再取倒 
    while(l > 0)
    {
        int num = l % 2;
        l = l / 2;
        A.push_back(num);
    }
    reverse(A.begin(),A.end());//注意取倒 
    while(r > 0)
    {
        int num = r % 2;
        r = r / 2;
        B.push_back(num); 
    }
    reverse(B.begin(),B.end());//注意取倒
    //当A的长度小于B的时候证明一定发生了进位,因此答案为0 
    if(A.size() < B.size())cout<<'0';
    //二者长度相等,则从最高位开始查找关键点 
    else if(A.size() == B.size())
    {
        //关键点即为A[i] ! = B[i]是idx为i的坐标点
        //说明i的下标位置发生了进位,因此从i往后的数全部为0 
        vector<int> C;
        int flag = 0;
        for(int i = 0;i < A.size();i ++ )//或者i < B.size()也可 
        {
            if(A[i] == B[i] && flag == 0)C.push_back(A[i]);//未到关键点时正常存储 
            else if( A[i] != B[i] && flag == 0)//关键点时存0并打标记 
            {
                flag = 1;
                C.push_back(0);
            }
            else if(flag == 1)C.push_back(0);//关键点之后利用标记继续存0 
        }
        //将C中存储的ans的二进制转换为十进制 
        ll ans = 0,w = 1;
        for(int i = C.size() - 1;i >= 0;i -- )
        {
            ans += w * C[i];
            w = w * 2;
        }
        cout<<ans;
        return 0;
    }
}

公式计算(交换律与结合律的推导)

题目描述

对于n个数a1,a2,a3.....an,计算公式[(a1&a1)|(a1&a2)|(a1&a3)|...(a1&an)]^[(a2&a1)|(a2&a2)|(a2&a3)...|(a2&an)]^...[(an&a1)|(an&a2)|(an&a3)...|(an&an)]

输入格式

第一行一个整数
第二行n个整数

输出格式

输出公式的结果

样例输入

2
1 1

样例输出

0

源代码 

若是暴力运算会使时间超限,所以必然存在优化的方法,也就是对于公式的简化。

根据结合律将每一个按位异或运算的式子看作一个个模块

那么可以发现第一个模块为a1&(a1|a2|a3|a4|a5......|an)

由此推出第n个模块为an(a1|a2|a3|a4|a5......|an)

好了现在由于(a1|a2|a3|a4|a5......|an)太长我们把它简化为s1

那么公式经过第一次简化过后我们可以发现(a1&s1)^(a2&s1).......(an&s1)

再进行合并可得s1&(a1^a2^a3.........^an)

我们再把(a1^a2^a3.........^an)简化为s2,因此整个公式我们就进行简化完毕了

ans=s1&s2; s1=(a1|a2|a3|a4|a5......|an); s2=(a1^a2^a3.........^an);

#include <iostream>
using namespace std;
const int N = 1000000+10;
int a[N];
typedef long long ll;
int main()
{
    int n;
    cin >> n;
    for(int i = 1;i <= n;i ++ )cin>>a[i];
    ll head = a[1];
    ll s = head;
    ll s1 = s,s2 = s;
    for(int i = 2;i <= n;i ++ )
    {
        s1 = (s1)|(a[i]);
        s2 = (s2)^(a[i]);
    }
    ll ans = (s1)&(s2);
    cout << ans;
    return 0;
}

真题改编 

改编自蓝桥杯省赛李白打酒题目 

汽车加油

题目描述

陈老师经常开车大街上行走,假设刚开始油箱里有T升汽油,每看见加油站陈老师就要把汽油的总量
翻倍(就是乘2);每看见十字路口气油就要减少1升;最后的时候陈老师的车开到一个十字路口,然
后车就没油了------就熄火了,陈老师好痛苦啊~~~!
然后他就开始回忆,一路上一共遇到n个加油站,m个十字路口,问造成这种惨烈的境遇有多少种可能?

输入格式

三个正整数T,n,m;

输出格式

输出有多少种可能

数据范围

1<=T=100
0<n+m<=18

样例输入

1 5 10

样例输出

10

源代码 
幂集:所谓幂集(Power Set), 就是原集合中所有的子集(包括全集和空集)构成的集族。可数集是最小的无限集; 它的幂集和实数集一一对应(也称同势),是不可数集。 不是所有不可数集都和实数集等势,集合的势可以无限的大。如实数集的幂集也是不可数集,但它的势比实数集大。 设X是一个有限集,|X| = k,根据二项式定理,X的幂集的势为2的k次方.

#include <iostream>
using namespace std;
typedef long long ll;
int main()
{
    ll t,n,m,ans = 0;
    cin >> t >> n >> m;
    //总共会有m+n个元素,也就是2^(m+n)-1种情况 
    for(int i = 0;i < (1ll << (n + m - 1));i ++ )//开始对于相应幂集的情况进行暴力枚举 
    {
        int sum = t;//临时变量转存t 
        int sumn = 0,summ = 0;//存路过加油站和路过十字路口的次数  
        for(int j = 0;j < n + m - 1;j ++ )//对于前m+n-1位的情况进行二进制枚举 
        {
            int num = i >> j & 1;
            if(num == 1)//加油站 
            {
                sum = sum * 2;//油箱翻倍 
                sumn ++ ;//加油站次数加一 
            }
            else if(num == 0)//十字路口 
            {
                sum -= 1;//油减去一升 
                summ ++ ;//十字路口次数加一 
            }
        }
        //当在前m+n-1个情况当中,有n次路过加油站,有m-1次路过十字路口,且油箱仅有一升油,那么就意味着最后一次必是十字路口且油耗尽 
        if(sum == 1&&sumn == n&&summ == m - 1)ans ++;
    } 
    cout << ans;//输出答案 
    return 0;
}

猜你喜欢

转载自blog.csdn.net/couchpotatoshy/article/details/125339714
今日推荐