算法竞赛进阶指南读书笔记——0x01位运算

位运算

算数位运算

二进制意义:

将二进制数的每一位看作旗帜

与:将二进制数的某一位设为0,可用于位的提取

或:将二进制数的某一位设为1,可用于大量布尔变量的保存

异或:1. x ⊕ x = 0 x \oplus x=0 xx=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,S0,S<0

设计原理(a,b为n位二进制数):

1.加减法统一: a − b = a + ( − b ) a-b=a+(-b) ab=a+(b),用加法表示减法

2.自然溢出:若 a + b ⩾ 2 n a+b\geqslant2^n a+b2n,则 a + b = a + b − 2 n a+b=a+b-2^n a+b=a+b2n

(溢出且至多溢出一次( 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+b2n<2(2n1)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)=(2n1+1)(a)=(2n1a)+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 111(2)=2n1,故将 2 n 2^n 2n拆为 2 n − 1 + 1 2^n-1+1 2n1+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=111(2)=2n1,即 2 n − 1 − a = ∼ a 2^n-1-a=\sim a 2n1a=∼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(0x000xFF)(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 1n=2n,n1=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) S1=n2n+1an=2n2nan=2S(S>0)

对于负数,易证 ( a ± b ) ≪ 1 = ( a ≪ 1 ) ± ( b ≪ 1 ) (a\pm b)\ll1=(a\ll1)\pm (b\ll1) (a±b)1=(a1)±(b1)(先进退位=先左移后进退位,第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=(2nS)1=2n+12S()

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+12S2n+12(2n11)>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+12S2n=2n2S=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} 2n2S2n1,即 S ⩽ 2 n − 2 S\leqslant2^{n-2} S2n2

那么 − S = 2 n − S ⩾ 2 n − 1 + 2 n − 2 -S=2^{n}-S\geqslant2^{n-1}+2^{n-2} S=2nS2n1+2n2,即该负数应满足第n-2位为1,才能保证左移后不发生溢出

反过来,若第n-2位为0,左移后符号位变为0,溢出为正数

算数右移 n ≫ 1 = ⌊ n 2.0 ⌋ n\gg 1=\lfloor \dfrac {n}{2.0}\rfloor n1=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} S1=(n1)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 S1=(n1+1)1=n=2S1=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=an2nbn=na2nbn=bn=1a2n

(拆分:化整为零,化幂为积,实现降次)

利用循环生成 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 111(2)=2n1),故复杂度为 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 ab=an2nbn=n2nabn=bn=12na化积为和

其中 2 n a = 2 n − 1 a ∗ 2 2^na=2^{n-1}a*2 2na=2n1a2

时间复杂度: 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=npnp(余数定义), ( a ∗ b ) % p = ( a % p ) ∗ ( b % p ) (a*b)\%p=(a\%p)*(b\%p) (ab)%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\ (*) (ab)%p=abpabp ()

( ∗ ) < 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{}类型转换 ab 溢出 除以p 类型转换,下面在 a ∗ b a*b ab溢出为负数的前提下考察这几个步骤:

溢出过程:将 a ∗ b a*b ab作整体考虑, a ∗ b a*b ab在补码意义下将表示负数。以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 npnp=n(⌊pn+1)p=npnpp

2.由 ( a ∗ b ) % p = ( a % p ) ∗ ( b % p ) (a*b)\%p=(a\%p)*(b\%p) (ab)%p=(a%p)(b%p)知,通过分别取模,在缩小数据范围的同时保证结果的正确性:取模后 a , b < p a,b<p a,b<p, a ∗ b < p 2 a*b<p^2 ab<p2;若此处不取模,则无法保证 a ∗ b < p 2 a*b<p^2 ab<p2.3处利用long double科学计数法的特性,将我们需要的精度保存下来;而若不对a,b进行范围限制,则 a ∗ b a*b ab位数过多,会丢失必要精度,导致自然溢出了不该溢出的位数从而得到错误结果。

二进制状态压缩

定义:用m位二进制数存储长度为m的 b o o l bool bool数组

基本元素:

≪ \ll :用于 10 … 0 ‾ ( 2 ) \overline{10…0}_{(2)} 100(2)的生成 ≫ \gg :用于数位的搬动

&:1.将数位置为0 2.将数位取出 |:1.将数位置为1 2.将数位放入 ⊕ \oplus :将数位切换

∼ \sim :将数位取反

1:用于操作的数位 -1:用于 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 111(2)的生成

操作:(n的处理) 操作符 蒙版

取第k位: ( n ≫ k ) (n\gg k) (nk)& 1 1 1

取第0-k-1位: n n n& ( − 1 + ( 1 ≪ k ) ) (-1+(1\ll k)) (1+(1k))

利用-1生成 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 111(2),再利用第k位的1使第 k ∼ n k \sim n kn位进位,第n位自然溢出,构成局部蒙版

取反第k位: n ⊕ ( 1 ≪ k ) n\oplus(1\ll k) n(1k)

第k位 置1: n ∣ ( 1 ≪ k ) n|(1\ll k) n(1k)

第k位 置0: n n n& ( ∼ ( 1 ≪ k ) ) (\sim (1\ll k)) ((1k))

利用&进行置0,需要 11 … 0 … 1 ‾ ( 2 ) \overline{11…0…1}_{(2)} 1101(2)的模板,可将 11 … 1 ‾ ( 2 ) \overline{11…1}_{(2)} 111(2)位于第k位的1减去,即

− 1 − ( 1 ≪ k ) = ∼ ( 1 ≪ k ) -1-(1\ll k)=\sim (1\ll k) 1(1k)=∼(1k)


例3最短Hamilton路径

思路1:暴搜

我们可以枚举每一种合法路径,计算出路径长度的最小值。

时间复杂度估计:

对于n个点,从0号点出发,第一次有n-1种选择,第二次n-2种,由此类推,共 ( n − 1 ) ! (n-1)! (n1)!种路径;对于每条路径上的每一个点,需要花费 O ( 1 ) O(1) O(1)的时间叠加边权,而每条路径都有 ( n − 1 ) (n-1) (n1)条边,故时间复杂度为 O ( n ∗ n ! ) O(n*n!) O(nn!)

思路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(1k))i)&1min{f[S(1k)][i]+w[i,k]}

由于确保k点一定被经过,故使用 ⊕ \oplus 将1置为0

时间复杂度估计:

状态空间 2 n ∗ n 2^n*n 2nn,转移花费 O ( n ) O(n) O(n),故时间复杂度为 O ( 2 n ∗ n 2 ) O(2^n*n^2) O(2nn2)

实现:

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(1k)始终小于等式左边的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)} 100(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&(1n+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

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值