关于位运算

diary

\(Hi\)
好久不见
由于文化科问题,退役了一会,结果发现 然并卵(大无语)
\(However\) 现在回归了 从退役到入门(开始填坑)

前置知识

运算符

众所周知,电脑是用二进制存储

二进制

相信大家都知道二进制的原理,这里我们主要用到十进制与二进制相互转换的原理。
举个例子,\(6\)的二进制是\(110\),那么\(6\)便可以标示成\(2^2+2^1\)

二进制或运算符\((or)\):符号为|,表示若两个二进制位都为0,则结果为0,否则为1。

二进制与运算符\((and)\):符号为&,表示若两个二进制位都为1,则结果为1,否则为0。

二进制否运算符\((not)\):符号为~,表示对一个二进制位取反

关于\(&\)的小妙招(其他的还有很多,可以自己find)
可以取出num在二进制表示下的最低位,例如x为奇数 \(x & 1 == 1\)则表示最低位为1,x为偶数 $ x & 1 == 0$ 则表示最低位为0。
\(n&((1<<k)-1)\)取出n(二进制)的$0 至k-1 $位

原码;反码;补码

原码,反码,补码 是三种二进制的表达方式

这三种表示方式里,最高位都代表的是符号位,1代表负数,0代表正数
例:

7 = 0000 0111
8 = 0000 1000
10= 0000 1010
-3 = 1000 0011
-8 = 1000 1000
-12=1000 1100

  • 正数的原码,反码,补码是一样的
  • 负数反码就是原码除了符号位不动,其他所有位按位取反

    例:

-7(原码)=1000 0111
-7(反码)=1111 1000

  • 负数的补码是反码加一得到的 (运算时包括符号位)<- 这点很重要,下面要讲
    例:

-7(原码)=1000 0111
-7(反码)=1111 1000
-7(补码)=1111 1001

移码

算术左移-逻辑左移

同一个东西。。。。。。
特点 : 高位丢弃,低位补零
example:

对于一个数字:00110011
左移一位的话:01100110

  • \(1<<n=2^n\)
  • $n<<1=2n $
  • 对于数值变化,左移n位,他们的数值就变为原来的2^n倍,左移一位,移位后的数值变为原来的2倍。

算术右移-逻辑右移

对于有符号的数,一般编译器都是对其进行算术右移,而对无符号数,都是采用逻辑右移,因为没有符号位,怎么移动都是补零,算术右移没有意义

逻辑右移:
对例子都进行逻辑右移一位,得01100110、00110011。只需要将数字往右移动一位,左边补上0就可以了。

算术右移 :(无符号的字符串,一般用不上(个人看法))
算术右移要保持符号位不变,左边所补上的数和符号位一样。对于有符号的数,最高位就是它的符号位,0表示正,1表示负。

ATTENTION
如果是对正数的右移的话,由于正数的原码和补码一样,所以你可以直接对原码进行移位操作,但是,如果你是对负数进行移位操作的话,由于一般负数都是以补码作为储存形式的,所以对负数的移位操作要对补码进行,而不是原码
example:

3 (原码)=0000 0011
3 (补码)=0000 0011
3 (右移一位)=0000 0001
-3 (原码)=1000 0011
-3 (补码)=1111 1101
-3 (右移1位)=1111 1110

所以

3右移一位=1 
巴特
-3右移一位=-2

例题起手

起床困难综合症

内容

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(2<=n<=10^5),m(2<=m<=10^9),\)表示$ drd$ 有 \(n\) 扇防御门,\(atm\) 的初始攻击力为 \(0 到 m\) 之间的整数。

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

输出格式

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

样例

3 10                         1
AND 5
OR 6
XOR 7

看似很难实则不难发现,肯肯定定是用二进制做的,因为造成伤害的各个二进制数字只与初始伤害的与之对应的各个二进制数字(造句不清,多多包涵(狗头))

可以从高到低循环,判断每一位是选1还是选0
选1有以下两种情况:

  • 1.在此位上加上这个数不大于m
  • 2.若选1造成的伤害大于选0造成的伤害(没有等于,因为等于时还不如选0,让之后的位数有选1的空间)

代码

#include <bits/stdc++.h>
using namespace std;
pair<string,int> a[100005];
int n,m;

int calc(int x,int bit){
	int temp;
	for(int i=1;i<=n;i++){
		temp=(a[i].second>>x)&1;
		if (a[i].first=="AND") bit=bit&temp;
		else if (a[i].first=="OR") bit=bit|temp;
		else bit=bit^temp;
	} 
	return bit;
}


int main(){
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++){
		char str[5];int t;
		scanf("%s%d",str,&t);
		a[i]=make_pair(str,t);
	}
	int val=0,ans=0;
	for (int x=30;x>=0;x--){
		int res0=calc(x,0);
		int res1=calc(x,1);
		if (val+(1<<x)<=m&&res1>res0){
			val+=(1<<x);ans+=(res1<<x);
		}
		else ans+=(res0<<x);
	}
	printf("%d",ans);
	return 0;
} 

快速幂

快速幂(\(Exponentiation by squaring\),平方求幂)是一种简单而有效的小算法,它可以以 \(O(log\) \(n)\) 的时间复杂度计算乘方。快速幂不仅本身非常常见,而且后续很多算法也都会用到快速幂。

让我们先来思考一个问题:7的10次方,怎样算比较快?

:最朴素的想法,\(7 * 7 = 49\)\(49 * 7 = 343\)、... 一步一步算

这样算\(too\) \(late\)了,尤其对计算机的CPU而言,每次运算只乘上一个个位数,无疑\(too\) \(bad\)了。这时我们想到,也许可以拆分问题。

先拆分10=0000 1010,则\(7^10\)次方\(=7^{1*2^3}*7^{1*2^1}\),,得出\(7^10\)

模仿这样的过程,我们得到一个在 \(O(log\) \(n)\) 时间内计算出幂的算法,也就是快速幂。

think

刚刚我们用到的,无非是一个拆分的思路。

先判断如果指数是奇数,就将 \(sum\)乘上当前的底数\(s\),否则偶数则不动;
在判断外 将底数翻倍 ,指数除以二

题目

Raising Modulo Numbers

内容

人是不同的。有些人偷偷看充满了有趣的女孩的杂志,有些人在地窖里制造原子弹,有些人喜欢使用WINDOWS,有些人喜欢数学游戏。最新的市场调查显示,这一细分市场目前被低估了,而且缺乏这样的游戏。这种游戏因此被纳入了科科达克运动。运动的遵循的规则:

每个玩家选择两个数字Ai和Bi,并把它们写在一张纸条上。其他人看不到这些数字。在一个特定的时刻,所有的玩家都会向其他玩家展示他们的数字。目标是确定包括自己在内的所有玩家的所有表达的总和,并用给定的数字M确定除法后的余数。获胜者是首先决定正确结果的人。根据玩家的经验,可以通过选择更高的数字来增加难度。

你应该编写一个程序来计算结果,并找出谁赢了这场比赛。

输入格式

输入由\(Z\)赋值组成。它们的数目由出现在输入的第一行上的单个正整数\(Z\)给出。然后分配如下。每个赋值都以包含一个整数\(M (1 <= M <= 45000)\)行开始。总数将除以这个数字。下一行包含玩家数\(H(1 <= H <= 45000)\)。接下来就是\(H\)行。在每一行上,正好有两个数字\(A_i\)\(B_i\)被空间隔开。两个数不能同时等于零。

输出格式

对于每个组合,只有一条输出线。在这条线上,有个数字,是表达的结果。

样例输入

3 
16 
4 
2 3 
3 4 
4 5 
5 6 
36123 
1 
2374859 3029382 
17 
1 
3 18132

样例输出

2 
13195 
13

思路:

快速幂思想,将\(b\)转化为二进制,有\(k\)位,第\(i(i<=0<k)\)位是\(c_i\)
\(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}\)

再运用公式\(a^{2^i}=(a^{2^{i-1}})^2\)

所以只要当循环(二进制)\(b\)的每一位,当\(c_i=1\)时就累加答案即可

代码

#include <iostream>
#include <cstdio>
#define LL long long
using namespace std;
int T,h;
LL MOD,a,b;
LL tot=0;
int main(){
	scanf("%d",&T);
	for (int i=1;i<=T;i++){
		tot=0;scanf("%lld%d",&MOD,&h);LL ans[45005];
		for (int k=1;k<=h;k++){
			ans[k]=1;scanf("%lld%lld",&a,&b);
			for (;b;b>>=1){
				if (b&1) ans[k]=ans[k]*a%MOD;a=a*a%MOD;}
			tot=(tot+ans[k])%MOD;}
		printf("%lld\n",tot);}
	return 0;}
posted @ 2022-06-27 10:04  hewt  阅读(31)  评论(1编辑  收藏  举报