2-7基础算法-位运算

一.基础
位运算经常考察异或的性质、状态压缩、与位运算有关的特殊数据结构、构造题。
位运算只能应用于整数,且一般为非负整数,不能应用于字符、浮点等类型。

左移操作相当于对原数进行乘以2的幂次方的操作,低位补0
右移操作相当于对原数进行除以2的幂次方的操作,高位补0

&与
|或
~按位取反
^按位异或

在讨论二进制数的位数时,通常采用的是从右向左的计数方法,其中最右边的位被称为第0位。

1.判断x的奇偶性:若x&1的结果是1,表示x二进制最后一位是1,则x是奇数;否则为偶数
2.获取x二进制中的第m位:右移m位,然后和1相与(取最后一位)。即x>>m&&1
3.将x的第i位改成1:1左移i位,和x相或,即x|(1< 4.将x的第i位改成0:构造出只有第i位是0,其他都是1,与x相与。即x&(~(1< 5.快速判断一个数字是否为2的幂次方:也就是x的二进制表示中只能有一个1。也就是x-1这位是0,往后全是1。我们将x和x-1相与,若为0,则是2的幂次方
6.获取二进制位中最低位(最右侧)的1:lowbit(x)。最低位的1及其右边都不动,左边全为0

二.例题
【例1】二进制中 1 的个数

2-7基础算法-位运算_第1张图片
评测系统

#include 
#include 
using namespace std;
int main() {
	int a[100] = { 0 };
	unsigned int x;//通常不希望处理负数的二进制表示,这里使用无符号整数
	cin >> x;
	int cnt = 0;
	while (x) { //进制转换
		a[cnt++] = x % 2;
		x = x / 2;
	}
	reverse(a, a + cnt);

	int ans = 0;//计数
	for (int i = 0; i < cnt; i++) {
		if (a[i] == 1)
			ans++;
	}
	cout << ans;
}

当然也可以不进行进制转换
使用x&(x-1)清除最后一位的1

#include 
#include 
using namespace std;
int main() {
	unsigned int x;
	cin >> x;
	int ans = 0;//计数
	while (x) {
		x = x & (x - 1); // 清除最低位的1
		ans++; // 计数器加1
	}
	cout << ans;
}

也可以不断右移,判断最后一位是否为1

#include 
#include 
using namespace std;
int main() {
	unsigned int x;
	cin >> x;
	int ans = 0;
	while (x) {
		if (x & 1)
			ans++;
		x =  x >> 1;
	}
	cout << ans;
}

也可以使用刚刚学到的lowbit(x)

#include 
#include 
using namespace std;
unsigned int lowbit(unsigned int x) { //固定代码
	return x & (-x);
}
int main() {
	unsigned int x,y;
	cin >> x;
	int ans = 0;
	while (x) {
		y = lowbit(x);
		ans++;
		x = x & (~y);//把x最后的1变为0
	}
	cout << ans;
}

【例2】区间或
2-7基础算法-位运算_第2张图片
2-7基础算法-位运算_第3张图片
评测系统

常规方法会超时,我们采用“拆位”
区间内所有二进制数的第0位若有1,则记为1,最终结果+20×1
区间内所有二进制数的第1位若有1,则记为1,最终结果+21×1
区间内所有二进制数的第2位若有1,则记为1,最终结果+22×1
区间内所有二进制数的第3位若没有1,则记为0,最终结果+23×0
判断第i位是否有1,可以通过观察这一位上所有二进制数的前缀和是否>0
注:2i可以用1<

#include 
#include 
using namespace std;
const int N = 1e5 + 5;
int main() {
	int n, q;
	cin >> n >> q;
	int a[N] = { 0 };
	int prefix[35][N] = { 0 };//记录前缀和
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	for (int i = 0; i <= 30; i++) {//从右往左第i位的前缀和
		for (int j = 1; j <= n; j++) {
			prefix[i][j] = prefix[i][j - 1] + (a[j] >> i & 1);//先移动i位,再确定最后一位是1还是0
		}
	}
	int l, r;
	while (q--) {
		int ans = 0;
		cin >> l >> r;
		for (int i = 0; i < 30; i++) {
			ans += (1 << i) * (prefix[i][r] - prefix[i][l - 1] > 0 ? 1 : 0);//2^i*1或0
		}
		cout << ans << endl;
	}
	return 0;
}

【例3】异或森林
2-7基础算法-位运算_第4张图片
评测系统
【解析仅供参考】
当我们求x的因数时,一般从1遍历到根号x,若根号x左侧有一个因数,则右侧也一定有一个因数。如16的因数是1,2,4,8,16,4左右各有2个因数。这使得完全平方数的因数总是有奇数个,而这个4,不与任何其他因数配对。对于非完全平方数,它们的因数总是成对出现的,没有任何一个因数能够单独存在而不与其他因数配对。所以因数个数为偶数个,则一定不是完全平方数。
即根号x是整数,则x一定是完全平方数,x的因数个数一定为奇数个

在给定范围内,完全平方数的个数通常小于非完全平方数的个数。如1~100内,完全平方数只有1、4、9、16、25、36、49、64、81、100
用总数减去完全平方数的个数就是偶数个因数的个数
总数应该为n*(n+1)/2
如5个数时,可选的子数组个数为5+4+3+2+1

a[i]不超过n,所有a[i]异或最终位数也不会改变,也就是不超过2n
2n是20000,根号下不到200

借助前缀异或和数组prexor,枚举所有的平方数,若某区间的异或和正好等于某个平方数,说明这个区间得到的结果是一个完全平方数
若满足sq==prexor[j]^prexor[i],说明区间[j+1,i]上的异或和是一个完全平方数
但这样我们需要遍历所有的i和j,时间复杂度太高

根据异或的性质,a^b=c可写为a^c=b,我们有prexor[j]==sq^prexor[i]
prexor[i]最大是1e4,sq最大是200,我们可以枚举所有的i和sq,统计所有小于i的j的个数(用cnt数组记录)

在构建prexor数组时,我们就可以记录哪些prexor[j]是合法的(存在的),若不存在就-0,存在就减去出现的次数(存在的子数组个数)

	for (int i = 1; i <= n; i++) {
		prexor[i] = prexor[i - 1] ^ a[i];
		cnt[prexor[i]]++;
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= 200; j++) {
			int sq = j * j;
			ans -= cnt[prexor[i] ^ sq];//cnt[prexor[j]]
		}
	}

但忽略了区间的左右端点关系,我们要保证j 可采用“滚动更新”的方式,如i=1时,表示1之后的j都不考虑(都为0)

	for (int i = 1; i <= n; i++) {
		prexor[i] = prexor[i - 1] ^ a[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= 200; j++) {
			int sq = j * j;
			ans -= cnt[prexor[i] ^ sq];//cnt[prexor[j]]
		}
		cnt[prexor[i]]++;
	}

另外prexor[0]也是合法的(左端点可以从0起),题目要求0的因数个数视为奇数,也是我们需要减掉的,所以cnt[0]应该为1
cnt选择异或和数组prexor作为下标,prexor最大不超过2n,所以将数组大小N调整为1e5+5

得到最终代码

#include 
#include 
using namespace std;
const int N = 1e5+5;
int main() {
	int n;
	cin >> n;
	int a[N], prexor[N] = { 0 };
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int cnt[N] = { 0 };
	cnt[0] = 1;
	for (int i = 1; i <= n; i++) {
		prexor[i] = prexor[i - 1] ^ a[i];
	}
	int ans = n*(n+1)/2;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= 200; j++) {
			int sq = j * j;
			ans -= cnt[prexor[i] ^ sq];//cnt[prexor[k]]
		}
		cnt[prexor[i]]++;
	}
	cout <<ans;
}

三.练习
1.最小的或运算
2-7基础算法-位运算_第5张图片
评测系统

分析:我们逐个分析a和b的二进制位。当a和b的二进制为都为0时,对应x的二进制位为0或1均可,为了最小我们取0。当a和b对应的二进制位为0和1时(或反之),x对应的二进制位为1才能保证相等。当a和b的二进制位都为1时,对应x的二进制位为0或1均可,为了最小我们取0。显然这是异或运算的结果。
注意long long和括号

#include 
#include 
using namespace std;
int main() {
    long long a,b;
    cin>>a>>b;
    cout<<(a^b);
}

2.简单的异或难题

2-7基础算法-位运算_第6张图片
2-7基础算法-位运算_第7张图片
评测系统

分析:两个相同的数异或是0,0和任何数异或都是这个数本身
所以区间内出现次数为偶数的数,不会影响最终结果
采用前缀异或和
2-7基础算法-位运算_第8张图片

#include 
#include 
using namespace std;
const int N = 1e5 + 5;
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n, m;
    int a[N], prexor[N];
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        prexor[i] = prexor[i - 1] ^ a[i];
    }
    while (m--) {
        int l, r;
        cin >> l >> r;
        cout << (prexor[l - 1] ^ prexor[r]) << endl;
    }
    return 0;
}

3.出列
2-7基础算法-位运算_第9张图片
2-7基础算法-位运算_第10张图片

评测系统

分析:
由表得,第k次出列时,会将二进制后k位为0的留下
又因为序号是连续从1开始排列的,最后留下的同学初始二进制序号一定为1后面k个0(也就是2k
2-7基础算法-位运算_第11张图片
我们希望找到一个最大的2k使其不超过同学个数n

#include 
#include 
using namespace std;
int main() {
    int n;
    cin >> n;
    int k = 0;
    while ((1 << k) <= n) {
        k++;
    }
    cout << (1 << (k - 1));
}

4.小蓝学位运算

2-7基础算法-位运算_第12张图片
2-7基础算法-位运算_第13张图片
评测系统

分析:
设a的前缀异或和数组为prexor,则原问题转化为求prexor[l-1]^prexor[r]

注意n>8192的情况,这时候一定会出现两个完全一样的数,他们的异或结果是0,使得最后连乘的结果也为0,这种情况要特殊考虑

#include 
#include 
using namespace std;
const int N = 1e6+5;//数组大小
const int N2 = 1e9 + 7;//模除
int main() {
    int n;
    cin >> n;
    int a[N], prexor[N];
    if (n > 8192) {  //特殊考虑
        cout << "0";
        return 0;
    }
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        prexor[i] = prexor[i - 1] ^ a[i];
    }
    long long ans = 1;//注意不要写成0
    for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j++) {
            long long xor_var = prexor[i - 1] ^ prexor[j];
            ans = (ans * xor_var) % N2;
        }
    }
    cout << ans;
    return 0;
}

5.位移
2-7基础算法-位运算_第14张图片
2-7基础算法-位运算_第15张图片
评测系统

分析:

010010
100100
左移右侧补0,右移左侧补0,我们删掉左右的0,中间的字符串若相等,就可以变换a
但不相等时也可以

a=1001
b=100000
将a右移1位,再左移3位就可得到b
从中间字符串的角度看,a变为1001,b变为1
得出,只要b是a的子串就可以
特殊的,b为0时恒成立

#include 
#include 
using namespace std;
string change(int x) {
    int left = 0, right = 31;
    while ((left<right)&&(((x>>right)&1)==0)) { //处理左侧0
        right--;
    }
    while ((left < right) && (((x >> left) & 1) == 0)) {  //处理右侧0
        left++;
    }
    string str;
    for (int i = right; i >= left; i--) {
        str += ((x >> i) & 1) + '0';//+ '0'将数字转为字符串类型
    }
    return str;
}
int main() {
    ios::sync_with_stdio(false);  //用cin/cout必须关闭流同步
    cin.tie(NULL);
    cout.tie(NULL);
    int t;
    cin >> t;
    while (t--) {
        int a, b;
        cin >> a >> b;
        if (b == 0) {
            cout << "Yes" << '\n';  //用endl会超时
            continue;
        }
        string aa = change(a);
        string bb = change(b);
        bool flag = 0;
        int cha = aa.size() - bb.size();
        for (int i = 0; i <= cha; i++) {
            flag = 1;
            for (int j = 0; j < bb.size(); j++) {
                if (aa[i+j] != bb[j]) {
                    flag = 0;
                    break;
                }
            }
            if (flag == 1) {
                cout << "Yes" << '\n';
                break;
            }
        }
        if(flag==0)
            cout << "No" << '\n';
    }
}

6.笨笨的机器人
2-7基础算法-位运算_第16张图片
评测系统

分析:
用0表示往左移动(用减表示),用1表示往右移动(用加表示)
在有n条指令的情况下,用n位的0或1表示每一个数字的状态
最终可能停留的位置由这一串n位的二进制数控制
这串二进制数可能的取值为0~n个1
也就是总数(分母)有2n种可能
这种情况也涵盖了可能跳回原点(也就是全0)的情况
而分子就是最终有多少串二进制数,使得最终停留位置是7的整数倍,我们用cnt计数
我们遍历所有可能的二进制位(从0到n个1),这串二进制数最右侧的数表示对a[0]的操作
注意输出结果要求四舍五入,而printf并不是严格的四舍五入,需要使用round函数四舍五入
round函数接受一个浮点数作为参数,四舍五入返回最接近的整数
如0.631938041会变为1.00000

#include 
#include  //round头文件
using namespace std;
const int N = 1e3 + 5;
int main() {
    int n;
    int a[N];
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    int cnt=0;//分子
    for (int i = 0; i <= (1 << n) - 1; i++) { //从 0 到 n个1 遍历
        int end = 0;//最终停留位置
        for (int j = 0; j < n; j++) {
            if (((i >> j) & 1) == 1) {
                end += a[j];  //是1往右移动
            }
            else {
                end -= a[j];  //是0往左移动
            }
        }
        if (end % 7 == 0) {
            cnt++;
        }
    }
    int fenmu = 1 << n;
//输入:9
//5 7 8 9 8 74 5 21 6
    double ans = (double)cnt / fenmu*10000;//1562.5000000000000
    ans = round(ans);//1563.0000000000000
    printf("%.4lf", ans/10000);
}

7.选题
2-7基础算法-位运算_第17张图片
2-7基础算法-位运算_第18张图片
评测系统

分析:有n个数字,n的规模很小,每个数字都有选和不选两种状态。我们用0表示不选,1表示选。在判断是否有3种不同的值时,使用map判断更简洁

#include 
#include 
#include 
using namespace std;
const int N = 50;
int main() {
    int n, l, r, x;
    cin >> n >> l >> r >> x;
    int a[N];
    for (int i = 0; i <= n; i++) {
        cin >> a[i];
    }
    int cnt=0;//计数
    map<int, int> q;//记录是否有3种不同的值
    for (int i = 0; i <= (1 << n)-1; i++) {  //从全0到n个1
        int maxn = 0, minn = 1e6+5, sum = 0;//注意min要给一个很大的值
        q.clear();//清空map
        for (int j = 0; j < n; j++) {
            if ((i >> j & 1) == 1) {
                q[a[j]] = 1;//给键赋值
                sum += a[j];
                maxn = max(maxn, a[j]);
                minn = min(minn, a[j]);
            }
        }
        if (sum >= l && sum <= r && maxn - minn >= x && q.size() >= 3) {
            cnt++;
        }
    }
    cout << cnt;
}

8.迷失之数
2-7基础算法-位运算_第19张图片
2-7基础算法-位运算_第20张图片
评测系统

分析:
A数组下标从1开始
输出第一个数一定是A数组中最大的那个数
用这个最大的数和A数组中其余的所有数相或,再减去这个最大的数,从前往后,找到那个最大的数,这就是要输出的第二个数
以此类推,每次都用上一轮的前缀或和结果与A数组中其余的所有数相或,再减去上一轮的前缀或和结果,找到那个最大的数并输出

#include 
using namespace std;
const int N = 1e6 + 5;
int main() {
	int n;
	cin >> n;
	int A[N];
	int maxA = -1;
	int maxAi = 0;
	for (int i = 1; i <= n; i++) {
		cin >> A[i];
		if (A[i] > maxA) {
			maxA = A[i];
			maxAi = i;
		}
	}
	int temp = A[maxAi];//记录前缀或和的值
	cout << maxA << " ";
	int determine[N];//判断当前数是否已被输出
	determine[maxAi] = 1;//记录已输出
	for (int j = 1; j < n; j++) {
		int maxn = -1;
		int maxi = 0;
		for (int i = 1; i <= n; i++) {
			if (determine[i]==1)
				continue;
			if (((temp | A[i]) - temp) > maxn) {
				maxn = (temp | A[i]) - temp;
				maxi = i;
			}
		}
		if (maxn == 0) {  //加速:如果后面剩的都是同一个数(或是没有意义的数),不用重复考虑
			break;
		}
		temp = temp | A[maxi];
		cout << A[maxi] << " ";
		determine[maxi] = 1;
	}
	for (int i = 1; i <= n; i++) { //按序输出剩余的数
		if (determine[i] == 0) {
			cout << A[i] << " ";
		}
	}
}

你可能感兴趣的:(C/C++算法竞赛,算法,c++,开发语言,c语言,青少年编程)