位运算
算数位运算
二进制意义:
将二进制数的每一位看作旗帜
与:将二进制数的某一位设为0,可用于位的提取
或:将二进制数的某一位设为1,可用于大量布尔变量的保存
异或:1. x ⊕ x = 0 x \oplus x=0 x⊕x=0 2.对二进制数的某一位切换,可用于操作二进制数
补码
定义: S ‾ = { S ( 2 ) , S ⩾ 0 , ∼ S ( 2 ) + 1 , S < 0 \overline S=\left\{\begin{array}{ll}S_{(2)}, & S\geqslant0,\\\sim S_{(2)}+1, &S<0\end{array}\right. S={S(2),∼S(2)+1,S⩾0,S<0
设计原理(a,b为n位二进制数):
1.加减法统一: a − b = a + ( − b ) a-b=a+(-b) a−b=a+(−b),用加法表示减法
2.自然溢出:若 a + b ⩾ 2 n a+b\geqslant2^n a+b⩾2n,则 a + b = a + b − 2 n a+b=a+b-2^n a+b=a+b−2n
(溢出且至多溢出一次( a + b − 2 n < 2 ( 2 n − 1 ) − 2 n < 2 n a+b-2^n<2(2^n-1)-2^n<2^n a+b−2n<2(2n−1)−2n<2n))
推导: − a = 0 − ( a ) = ① 2 n + 0 − ( a ) = ② ( 2 n − 1 + 1 ) − ( a ) = ( 2 n − 1 − a ) + 1 = ③ ∼ a + 1 ( 其中 a > 0 ) -a=0-(a)\overset{①}{=}2^n+0-(a)\overset{②}{=}(2^n-1+1)-(a)=(2^n-1-a)+1\overset{③}{=}\sim a+1(其中a>0) −a=0−(a)=①2n+0−(a)=②(2n−1+1)−(a)=(2n−1−a)+1=③∼a+1(其中a>0)
①由加法的自然溢出,减法可向第 n + 1 n+1 n+1位(溢出位)借位至多1次
②n位二进制数能表示的最大的数为 11 … 1 ‾ ( 2 ) = 2 n − 1 \overline {11…1}_{(2)}=2^n-1 11…1(2)=2n−1,故将 2 n 2^n 2n拆为 2 n − 1 + 1 2^n-1+1 2n−1+1,进而调整运算次序
③ ∼ a \sim a ∼a与 a a a每一位各为0和1,故 a + ∼ a = 11 … 1 ‾ ( 2 ) = 2 n − 1 a+\sim a=\overline {11…1}_{(2)}=2^n-1 a+∼a=11…1(2)=2n−1,即 2 n − 1 − a = ∼ a 2^n-1-a=\sim a 2n−1−a=∼a
注:推导过程中未需考虑符号位,因为符号位为负数的原码表示下赋予的意义,在利用正数推导补码时无需考虑
推广:负数补码表示转换为原码表示:利用 0 − ( − a ) = a ( a > 0 ) 0-(-a)=a(a>0) 0−(−a)=a(a>0),将负号提出
应用:int数组无穷大初始化
int变量的最大值: 01111111 11111111 11111111 11111111 ‾ ( 2 ) = 0 x 7 F F F F F F F \overline{01111111\ 11111111\ 11111111\ 11111111}_{(2)}=0x7F\ FF\ FF\ FF 01111111 11111111 11111111 11111111(2)=0x7F FF FF FF
( 0111 1111 ‾ ( 2 ) = 0 x 7 F ) (\overline{0111\ 1111}_{(2)}=0x7F) (0111 1111(2)=0x7F)
m e m s e t ( a , v a l , s i z e o f ( a ) ) memset(a, val, sizeof(a)) memset(a,val,sizeof(a))语句:将 v a l ( 0 x 00 ∼ 0 x F F ) val(0x00 \sim 0xFF) val(0x00∼0xFF)(8 bit)填充到数组a的每一个字节(byte)上
因此, 0 x 7 F 7 F 7 F 7 F 0x7F7F7F7F 0x7F7F7F7F为 m e m s e t memset memset语句能初始化出的最大数值
技巧:选择 0 x 3 F 3 F 3 F 3 F 0x3F3F3F3F 0x3F3F3F3F做无穷大,使得 2 I N F < I N T _ M A X 2INF<INT\_MAX 2INF<INT_MAX
memset(a,0x3f,sizeof(a));
移位运算
算数意义:
左移: 1 ≪ n = 2 n , n ≪ 1 = 2 n 1\ll n=2^n,n\ll 1=2n 1≪n=2n,n≪1=2n(高位越界舍弃)
略证:
对于正数, S ≪ 1 = ∑ n 2 n + 1 a n = 2 ∑ n 2 n a n = 2 S ( S > 0 ) S\ll1=\sum\limits_{n}2^{n+1}a_{n}=2\sum\limits_{n}2^{n}a_{n}=2S(S>0) S≪1=n∑2n+1an=2n∑2nan=2S(S>0)
对于负数,易证 ( a ± b ) ≪ 1 = ( a ≪ 1 ) ± ( b ≪ 1 ) (a\pm b)\ll1=(a\ll1)\pm (b\ll1) (a±b)≪1=(a≪1)±(b≪1)(先进退位=先左移后进退位,第0位不影响高位进退位)
设该n位负数为 − S ( S > 0 ) -S(S>0) −S(S>0),则 ( − S ) ≪ 1 = ( 2 n − S ) ≪ 1 = 2 n + 1 − 2 S ( ∗ ) (-S)\ll1=(2^n-S)\ll1=2^{n+1}-2S(*) (−S)≪1=(2n−S)≪1=2n+1−2S(∗)
而 2 n + 1 − 2 S ⩾ 2 n + 1 − 2 ( 2 n − 1 − 1 ) > 2 n 2^{n+1}-2S\geqslant2^{n+1}-2(2^{n-1}-1)>2^n 2n+1−2S⩾2n+1−2(2n−1−1)>2n(正数符号位为0)
故发生且仅发生一次溢出, ( ∗ ) = 2 n + 1 − 2 S − 2 n = 2 n − 2 S = − 2 S (*)=2^{n+1}-2S-2^n=2^n-2S=-2S (∗)=2n+1−2S−2n=2n−2S=−2S
进一步地,若 − 2 S -2S −2S能够被表达(即本身未溢出),则 − 2 S < 0 -2S<0 −2S<0,即符号位为1,那么 2 n − 2 S ⩾ 2 n − 1 2^n-2S\geqslant2^{n-1} 2n−2S⩾2n−1,即 S ⩽ 2 n − 2 S\leqslant2^{n-2} S⩽2n−2
那么 − S = 2 n − S ⩾ 2 n − 1 + 2 n − 2 -S=2^{n}-S\geqslant2^{n-1}+2^{n-2} −S=2n−S⩾2n−1+2n−2,即该负数应满足第n-2位为1,才能保证左移后不发生溢出
反过来,若第n-2位为0,左移后符号位变为0,溢出为正数
算数右移: n ≫ 1 = ⌊ n 2.0 ⌋ n\gg 1=\lfloor \dfrac {n}{2.0}\rfloor n≫1=⌊2.0n⌋(高位填符号位)
c++整数除法向0取整
略证:
若 S = 2 n S=2n S=2n,则 S ≫ 1 = ( n ≪ 1 ) ≫ 1 = n = S 2 S\gg1=(n\ll1)\gg1=n=\dfrac{S}{2} S≫1=(n≪1)≫1=n=2S
若 S = 2 n + 1 S=2n+1 S=2n+1,则 S ≫ 1 = ( n ≪ 1 + 1 ) ≫ 1 = n = S − 1 2 = ⌊ S 2.0 ⌋ S\gg1=(n\ll1+1)\gg1=n=\dfrac{S-1}{2}=\lfloor \dfrac {S}{2.0}\rfloor S≫1=(n≪1+1)≫1=n=2S−1=⌊2.0S⌋
逻辑右移:高位填0
例1 a^b
思路:二进制拆分
对指数b进行二进制拆分,即 a b = a ∑ n 2 n b n = ∏ n a 2 n b n = ∏ b n = 1 a 2 n a^b=a^{\sum\limits_{n}2^{n}b_{n}}=\prod\limits_{n}a^{2^nb_n}=\prod\limits_{b_n=1}a^{2^n} ab=an∑2nbn=n∏a2nbn=bn=1∏a2n
(拆分:化整为零,化幂为积,实现降次)
利用循环生成 a 2 i a^{2i} a2i,即 a 2 i = ( a i ) 2 a^{2i}=(a^{i})^2 a2i=(ai)2
判断 b b b的二进制表示每一位是否为1:将 b b b不断右移,利用 b b b&1取出第0位
时间复杂度估计:
b b b的位数不超过 ⌈ l o g 2 ( b + 1 ) ⌉ \lceil log_2(b+1)\rceil ⌈log2(b+1)⌉(n位二进制数最大为 11 … 1 ‾ ( 2 ) = 2 n − 1 \overline{11…1}_{(2)}=2^n-1 11…1(2)=2n−1),故复杂度为 O ( l o g 2 b ) O(log_2b) O(log2b)
实现:
int a, b, p;
cin >> a >> b >> p;
int ans = 1 % p; // 不写ans=1:防止b=0,p=1的情况,此时无法进入循环,而ans=0
for (; b; b >>= 1) { // 遍历b二进制表示的每一位
if (b & 1) ans = (long long)ans * a % p; // 不写*=:方便取模
a = (long long)a * a % p; // 计算时变量类型与保存结果的变量类型无关,需强制类型转换
}
cout << ans << endl;
例264位整数乘法
思路1:二进制拆分
对乘数b进行二进制拆分,即 a ∗ b = a ∑ n 2 n b n = ∑ n 2 n a b n = ∑ b n = 1 2 n a a*b=a\sum \limits_{n}2^nb_n=\sum \limits _{n}2^nab_n=\sum \limits_{b_n=1}2^na a∗b=an∑2nbn=n∑2nabn=bn=1∑2na(化积为和)
其中 2 n a = 2 n − 1 a ∗ 2 2^na=2^{n-1}a*2 2na=2n−1a∗2
时间复杂度: O ( l o g 2 b ) O(log_2 b) O(log2b)
实现:
long long a, b, p;
cin >> a >> b >> p;
long long ans = 0;
for (; b; b >>= 1) {
if (b & 1) ans = (ans + a) % p;
a = a * 2 % p;
}
cout << ans << endl;
思路2:数论
易知: n % p = n − ⌊ n p ⌋ ∗ p n\%p=n-\lfloor\dfrac{n}{p}\rfloor*p n%p=n−⌊pn⌋∗p(余数定义), ( a ∗ b ) % p = ( a % p ) ∗ ( b % p ) (a*b)\%p=(a\%p)*(b\%p) (a∗b)%p=(a%p)∗(b%p)(对a,b做带余除法,代入)
故 ( a ∗ b ) % p = a ∗ b − ⌊ a ∗ b p ⌋ ∗ p ( ∗ ) (a*b)\%p=a*b-\lfloor\dfrac{a*b}{p}\rfloor*p\ (*) (a∗b)%p=a∗b−⌊pa∗b⌋∗p (∗)
又 ( ∗ ) < p (*)<p (∗)<p,可使两项自然溢出而得到正确结果
实现:
long long a, b, p;
cin>>a>>b>>p;
a %= p, b %= p; // 2.
long long c = (long double)a * b / p; // 1.
long long ans = a * b - c * p;
if (ans < 0) ans += p;
cout << ans << endl;
1.此处有溢出的可能性。该过程可以表示为: a ∗ b → 溢出 → 除以 p → 类型转换 a*b\xrightarrow{}溢出\xrightarrow{}除以p\xrightarrow{}类型转换 a∗b溢出除以p类型转换,下面在 a ∗ b a*b a∗b溢出为负数的前提下考察这几个步骤:
溢出过程:将 a ∗ b a*b a∗b作整体考虑, a ∗ b a*b a∗b在补码意义下将表示负数。以3位补码为例,按二进制由小到大表示的数依次为: 0 , 1 , 2 , 3 , − 4 , − 3 , − 2 , − 1 0,1,2,3,-4,-3,-2,-1 0,1,2,3,−4,−3,−2,−1,即构成 − 4 , − 3 , − 2 , − 1 , 0 , 1 , 2 , 3 -4,-3,-2,-1,0,1,2,3 −4,−3,−2,−1,0,1,2,3的循环,故自然溢出并未改变数字的相对大小。
除法过程:尽管我们不清楚除法的底层实现,但从其算数效果来看,符号位将影响计算方式(类比算数右移),具体表现为负数除以正数,结果仍为负数。
类型转换:将实数转换为整数的底层实现是去掉小数位,而去掉小数位对正数的影响是缩小,但对负数而言是放大(向0取整),这是导致相对大小发生变化的根源。
由此可见,由于类型转换向0取整的特性,导致实际计算结果与我们期望的向下取整有了出入。有两条解决思路:一是将a,b的变量类型指定为unsigned;二是对计算结果进行讨论,若结果为负,则补一个p:
n − ⌈ n p ⌉ ∗ p = n − ( ⌊ n p ⌋ + 1 ) ∗ p = n − ⌊ n p ⌋ ∗ p − p n-\lceil\dfrac{n}{p}\rceil*p=n-(\lfloor\dfrac{n}{p}\rfloor+1)*p=n-\lfloor\dfrac{n}{p}\rfloor*p-p n−⌈pn⌉∗p=n−(⌊pn⌋+1)∗p=n−⌊pn⌋∗p−p
2.由 ( a ∗ b ) % p = ( a % p ) ∗ ( b % p ) (a*b)\%p=(a\%p)*(b\%p) (a∗b)%p=(a%p)∗(b%p)知,通过分别取模,在缩小数据范围的同时保证结果的正确性:取模后 a , b < p a,b<p a,b<p, a ∗ b < p 2 a*b<p^2 a∗b<p2;若此处不取模,则无法保证 a ∗ b < p 2 a*b<p^2 a∗b<p2.3处利用long double科学计数法的特性,将我们需要的精度保存下来;而若不对a,b进行范围限制,则 a ∗ b a*b a∗b位数过多,会丢失必要精度,导致自然溢出了不该溢出的位数从而得到错误结果。
二进制状态压缩
定义:用m位二进制数存储长度为m的 b o o l bool bool数组
基本元素:
≪ \ll ≪:用于 10 … 0 ‾ ( 2 ) \overline{10…0}_{(2)} 10…0(2)的生成 ≫ \gg ≫:用于数位的搬动
&:1.将数位置为0 2.将数位取出 |:1.将数位置为1 2.将数位放入 ⊕ \oplus ⊕:将数位切换
∼ \sim ∼:将数位取反
1:用于操作的数位 -1:用于 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 11…1(2)的生成
操作:(n的处理) 操作符 蒙版
取第k位: ( n ≫ k ) (n\gg k) (n≫k)& 1 1 1
取第0-k-1位: n n n& ( − 1 + ( 1 ≪ k ) ) (-1+(1\ll k)) (−1+(1≪k))
利用-1生成 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 11…1(2),再利用第k位的1使第 k ∼ n k \sim n k∼n位进位,第n位自然溢出,构成局部蒙版
取反第k位: n ⊕ ( 1 ≪ k ) n\oplus(1\ll k) n⊕(1≪k)
第k位 置1: n ∣ ( 1 ≪ k ) n|(1\ll k) n∣(1≪k)
第k位 置0: n n n& ( ∼ ( 1 ≪ k ) ) (\sim (1\ll k)) (∼(1≪k))
利用&进行置0,需要 11 … 0 … 1 ‾ ( 2 ) \overline{11…0…1}_{(2)} 11…0…1(2)的模板,可将 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 11…1(2)位于第k位的1减去,即
− 1 − ( 1 ≪ k ) = ∼ ( 1 ≪ k ) -1-(1\ll k)=\sim (1\ll k) −1−(1≪k)=∼(1≪k)
例3最短Hamilton路径
思路1:暴搜
我们可以枚举每一种合法路径,计算出路径长度的最小值。
时间复杂度估计:
对于n个点,从0号点出发,第一次有n-1种选择,第二次n-2种,由此类推,共 ( n − 1 ) ! (n-1)! (n−1)!种路径;对于每条路径上的每一个点,需要花费 O ( 1 ) O(1) O(1)的时间叠加边权,而每条路径都有 ( n − 1 ) (n-1) (n−1)条边,故时间复杂度为 O ( n ∗ n ! ) O(n*n!) O(n∗n!)
思路2:状压DP
思路1复杂度过大的根本原因是我们记录了冗余数据:点的访问顺序。由于我们只需求解最短路径,所以无需关心具体走法,可以设置简化的状态,降低时间复杂度。
首先,由于我们不需要考虑点的访问顺序,我们可以仅记录所有点的访问状态,又由于每个点仅有访问和不访问两种状态,故可以使用二进制进行状态压缩。由于答案对访问的末端点进行了要求,故我们需要将末端点编号加入状态中,以区分访问状态相同但末端点不同的两个状态。
由此,我们设计出如下状态:
f [ S ] [ k ] f[S][k] f[S][k]:访问状态为S且末端点位于k的最短路径长度,边界为 f [ 1 ] [ 0 ] = 0 f[1][0]=0 f[1][0]=0
接着考虑状态转移。从状态本身出发,我们忽略访问顺序,对于状态 f [ S ] [ k ] f[S][k] f[S][k]的转移,可以理解为:
●●●●●●|● → \xrightarrow{} ●●●●●o → \xrightarrow{} min{●’●●●●o|●’ + w(●’,o)}
写作数学表达式即
f [ S ] [ k ] = min ( ( S ⊕ ( 1 ≪ k ) ) ≫ i ) & 1 { f [ S ⊕ ( 1 ≪ k ) ] [ i ] + w [ i , k ] } f[S][k]=\min\limits_{((S\oplus(1\ll k))\gg i)\&1}\{f[S\oplus(1\ll k)][i]+w[i,k]\} f[S][k]=((S⊕(1≪k))≫i)&1min{f[S⊕(1≪k)][i]+w[i,k]}
由于确保k点一定被经过,故使用 ⊕ \oplus ⊕将1置为0
时间复杂度估计:
状态空间 2 n ∗ n 2^n*n 2n∗n,转移花费 O ( n ) O(n) O(n),故时间复杂度为 O ( 2 n ∗ n 2 ) O(2^n*n^2) O(2n∗n2)
实现:
memset(f, 0x3f, sizeof(f)); // 1.
f[1][0] = 0;
for (int S = 1; S < (1 << n); S++) // 2.
for (int k = 0; k < n; k++)
if ((S >> k) & 1)
for (int i = 0; i < n; i++)
if (((S ^ (1 << k)) >> i) & 1)
f[S][k] = min(f[S ^ (1 << k)][i] + w[i][k], f[S][k]);
细节:
1.由于状态转移方程的策略为取min,为防止初值对结果的干扰,将f初始化为无穷大
2.状态转移方程等式左边的值从等式右边获得,故要求等式右边先于等式左边计算;观察转移方程,不难发现等式右边 S ⊕ ( 1 ≪ k ) S \oplus (1\ll k) S⊕(1≪k)始终小于等式左边的S,故要求S始终按照递增的顺序进行枚举,所以将S放在外层循环保证S始终递增
例4起床困难综合症
思路:模拟
不难发现三种按位操作的独立性:对每一位进行&、|、 ⊕ \oplus ⊕操作后,均不对高位产生影响。
在这基础上,我们只需要对初始攻击力二进制表示的每一位进行讨论,便能得到最大的伤害。一般地,若能证明某数的各个数位互相独立,就不用考虑各个数位各种情况的组合,时间复杂度将由 O ( n ) O(n) O(n)优化至 O ( l o g n ) O(logn) O(logn).
故时间复杂度为 O ( n l o g 2 m ) O(nlog_2m) O(nlog2m)
实现:
bool calc(int pos, bool bit) {
for (int i = 0; i < n; i++) {
bool x = ((t[i] >> pos) & 1);
if (ops[i] == 0)
bit &= x;
else if (ops[i] == 1)
bit |= x;
else
bit ^= x;
}
return bit;
}
int main() {
string op;
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> op >> t[i];
if (op == "AND")
ops[i] = 0;
else if (op == "OR")
ops[i] = 1;
else
ops[i] = 2;
}
for (int pos = 29; pos >= 0; pos--) { // 1.
bool res0 = calc(pos, 0), res1 = calc(pos, 1);
if (m >= (1 << pos) && res1 > res0) // 2.
m -= (1 << pos), ans += (res1 << pos);
else
ans += (res0 << pos);
}
cout << ans << endl;
return 0;
}
细节:
1.此处采用贪心策略,先填高位,后填低位:由于高位带来的贡献大于低位,尽可能的在高位处多填1,使结果最大化;pos从29开始递减,是因为n最大为 1 0 9 10^9 109,二进制表示最多30位,最高位最大为第29位。
2.此处采用贪心策略,若不是一定填1,则选择填0:若高位填0或1带来的结果一致,则优先选择填0,为低位选择留出更多空间。
注:此时初始攻击力的选择可能有多解(例如最后一层AND 0),而最大伤害值的解唯一:若能填1,则一定填1;若不能填1,则填0。
成对变换
利用 ⊕ \oplus ⊕位切换的性质,对n的第0位进行切换,形成偶数加一,奇数减一的效果。
lowbit运算
定义:非负整数n二进制表示下最低位1及后边所有0构成的数值。
推导:差异化
利用定义给出的 … 10 … 0 ‾ ( 2 ) \overline{…10…0}_{(2)} …10…0(2)中的1,使1前和1后发生不同的变化。
差异化根源:进位
思路:
将n取反,加一则产生进位,由于原位置的1取反后化为0,将进位截断,故高于1的位不改变取反状态。此时该数与原数低于1的位完全相同,高于1的位完全相反。再与原数做&,即可取出lowbit.
写作数学表达式:
l o w b i t ( n ) = n & ( ∼ n + 1 ) = n & ( − 1 − n + 1 ) = n & ( − n ) lowbit(n)=n\&(\sim n+1)=n\&(-1-n+1)=n\&(-n) lowbit(n)=n&(∼n+1)=n&(−1−n+1)=n&(−n)
应用:取出二进制中所有的1
将n不断减去lowbit(n),不断移除此时最低为的1
1的位数为 l o g 2 l o w b i t ( n ) log_2lowbit(n) log2lowbit(n),利用哈希将对数表准备好
GCC内置相关函数:
int __builtin_ctz(unsigned int x) 返回x二进制表示下最低位1后有多少0
int __builtin_popcount(unsigned int x) 返回x二进制表示下有多少位为1