位运算,树状数组

位运算

基于二进制的运算,有按位与、按位或、按位异或、按位取反、左移、右移,这六种基本操作。

按位与

只有两个都是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);
	}
}

例题

例题①
+解析

例题②
+解析

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值