位运算
基于二进制的运算,有按位与、按位或、按位异或、按位取反、左移、右移,这六种基本操作。
按位与
只有两个都是1才是1,其他情况都是0。
按位或
两个只要有一个1就是1。
按位异或
两个位置不同为1,相同为0。
按位取反
所有位置转置。
按位左移
所有位置向左移动,溢出的高位消失,低位补0。
按位右移
所有位置向右移动,溢出的低位消失,高位补0。
使用经验
键盘按键
这六种位运算符在C/C++中可以直接通过键盘打出,按住shift键再按下对应的键即可。
优先级问题
如果没有记清楚运算符之间的优先级,最好还是在位运算的时候加上括号,保证运行的结果符合预期。
例如:
a左移两位后再加上b,写成:
a = a << 2 + b
实际上由于加号优先于左移操作,所以结果上是a左移2+b位。
位运算基本操作
由于2进制是计算机最擅长的(01),所以位运算的速度非常快,很多时候都可以用位运算来代替加减乘除运算。
取出倒数第K位的位
int bit = (a & (1<< k)) > 0 // k=0时可以用来判断奇偶
让第K位变为1
a = a | (1<< k)
让第K位转置
a = a ^ (1<< k)
让第K位变为0
a = (a | (1<< k))^(1<< k)
异或同一个数两次不变
a ^ b ^ b == a
按位取反注意符号
int a;
~a == - a - 1
左右移
a<<1 == a*2
a<<2 == a*4
a<<3 == a*8
a>>1 == a/2
a>>2 == a/4
a>>3 == a/8
a*10 == (a<<1) + (a<<3)
统计二进制下1的个数
int tmp=a;
int cnt=0;
while(tmp){
if(tmp&1)cnt++;
tmp>>=1;
}
判断A是不是B的子集
A | B == B
枚举子集
// son of sta
for(int x=sta;;){
...
if(x==0)break;
x=(x-1)&sta;
}
其他操作
int main() {
bitset<8>B(243),ngB(-243),addB(244),subB(242),tmp;
cout<<B<<endl; // 11110011
/// 位置最低的1(lowbit):b & (-b)
tmp=B;tmp&=ngB;
cout<<tmp<<endl; // 00000001
/// 位置最低的1变为0:b & (b-1)
tmp=B;tmp&=subB;
cout<<tmp<<endl; // 11110010
/// 位置最低的0变为1:b | (b+1)
tmp=B;tmp|=addB;
cout<<tmp<<endl; // 11110111
/// 右边连续1变为0:b & (b+1)
tmp=B;tmp&=addB;
cout<<tmp<<endl; // 11110000
/// 右边连续0变为1:b | (b-1)
tmp=B;tmp|=subB;
cout<<tmp<<endl; // 11110011
}
重点讲一下lowbit
int lowbit(int a){
return a&(-a);
}
树状数组
初步认识
我们先看一下图
A数组代表普通的数组,C数组代表树状数组,有什么不同呢?
A数组的话,A[i]就是第i个数的值,而C[i]是图中所示,往下可以延伸的所有点的总和。比如 C [ 4 ] = A [ 1 ] + . . . + A [ 4 ] , C [ 6 ] = A [ 5 ] + A [ 6 ] C[4]=A[1]+...+A[4],C[6]=A[5]+A[6] C[4]=A[1]+...+A[4],C[6]=A[5]+A[6]。
看到这里,你们可能有两个疑问,第一,这个取值的地方和前缀和有什么不同,第二,为什么不同的C[i]代表不同数量数的和。
前缀和在查询区间和方面确实比这个好用,也方便多了,但是对于修改,前缀和是O(n),树状数组是O(logn),也就是说需要修改的次数比较多时用树状数组更好。
至于第二个疑问,先卖个关子,请问为什么说它是树状呢?我们上个图
这个就是一个二叉树的模型,记住这个图,我们对此变形得到下图
所以树状数组就是用二叉树的数组表示,对于第二张图,定义终端的顶点为C数组。
C[i]代表 子树的叶子结点的权值之和
可以知道
C
[
1
]
=
A
[
1
]
;
C
[
2
]
=
A
[
1
]
+
A
[
2
]
;
C
[
3
]
=
A
[
3
]
;
C
[
4
]
=
A
[
1
]
+
A
[
2
]
+
A
[
3
]
+
A
[
4
]
;
C
[
5
]
=
A
[
5
]
;
C
[
6
]
=
A
[
5
]
+
A
[
6
]
;
C
[
7
]
=
A
[
7
]
;
C
[
8
]
=
A
[
1
]
+
A
[
2
]
+
A
[
3
]
+
A
[
4
]
+
A
[
5
]
+
A
[
6
]
+
A
[
7
]
+
A
[
8
]
;
C[1]=A[1];\\ C[2]=A[1]+A[2];\\ C[3]=A[3];\\ C[4]=A[1]+A[2]+A[3]+A[4];\\ C[5]=A[5];\\ C[6]=A[5]+A[6];\\ C[7]=A[7];\\ C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
C[1]=A[1];C[2]=A[1]+A[2];C[3]=A[3];C[4]=A[1]+A[2]+A[3]+A[4];C[5]=A[5];C[6]=A[5]+A[6];C[7]=A[7];C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
将C[]数组的结点序号转化为二进制
1=(001) C[1]=A[1];
2=(010) C[2]=A[1]+A[2];
3=(011) C[3]=A[3];
4=(100) C[4]=A[1]+A[2]+A[3]+A[4];
5=(101) C[5]=A[5];
6=(110) C[6]=A[5]+A[6];
7=(111) C[7]=A[7];
8=(1000) C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
对照式子可以发现 C [ i ] C[i] C[i]所代表的数的个数为上面讲到的 l o w b i t ( i ) lowbit(i) lowbit(i)
区间查询
例子:
s
u
m
[
7
]
=
A
[
1
]
+
A
[
2
]
+
A
[
3
]
+
A
[
4
]
+
A
[
5
]
+
A
[
6
]
+
A
[
7
]
;
C
[
4
]
=
A
[
1
]
+
A
[
2
]
+
A
[
3
]
+
A
[
4
]
;
C
[
6
]
=
A
[
5
]
+
A
[
6
]
;
C
[
7
]
=
A
[
7
]
;
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ; \\ C[4]=A[1]+A[2]+A[3]+A[4]; \\ C[6]=A[5]+A[6];\\ C[7]=A[7];
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7];C[4]=A[1]+A[2]+A[3]+A[4];C[6]=A[5]+A[6];C[7]=A[7];
可以推出:
s
u
m
[
7
]
=
C
[
4
]
+
C
[
6
]
+
C
[
7
]
sum[7]=C[4]+C[6]+C[7]
sum[7]=C[4]+C[6]+C[7]
序号写为二进制:
s
u
m
[
(
111
)
]
=
C
[
(
100
)
]
+
C
[
(
110
)
]
+
C
[
(
111
)
]
sum[(111)]=C[(100)]+C[(110)]+C[(111)]
sum[(111)]=C[(100)]+C[(110)]+C[(111)]
是不是可以猜到 s u m [ ( 11001 ) ] sum[(11001)] sum[(11001)]了呢?是不是应该是 C [ ( 10000 ) ] + C [ ( 11000 ) ] + C [ ( 11001 ) ] C[(10000)]+C[(11000)]+C[(11001)] C[(10000)]+C[(11000)]+C[(11001)]呢?
验证一下,发现确实是这样。
树状数组追其根本就是二进制的应用
求和代码
int query(int p){
int ans=0;
while(p)
ans+=tr[p],
p-=lowbit(p);
return ans;
}
单点更新
发现如果对
A
[
5
]
A[5]
A[5]更新,会变动的有
C
[
5
]
,
C
[
6
]
,
C
[
8
]
C[5],C[6],C[8]
C[5],C[6],C[8],即对A[i]更新时,需要从A[i]顶端(图中)的C[i]开始,往上更新。
而对于C[i],它的上一个应该是 C [ i + l o w b i t [ i ] ] C[i+lowbit[i]] C[i+lowbit[i]],用二进制说明就是,从后面开始,把第一个一的位置-1,这个位置往前的位置+1。
11010->11100->100000
这个过程可以看成查询的逆过程
代码:
void update(int p,int val){
while(p<=n){
tr[p]+=val;
p+=lowbit(p);
}
}