学习笔记(23.4.23)树状数组,线段树

 树状数组

树状数组的作用是在O(logn)进行单点修改,区间修改,并可以在O(logn)的时间内查询区间内数据的和。它的基本原理和二叉树很像。

 每一个红色框内保存的内容为其所有子节点的权重的和,树状数组中节点x的父节点为x+lowbit(x)(lowbit(x)运算的作用是取得x在二进制表达中第一个1所在位置),同一层的节点为x-lowbit(x)。

 代码模板

​
#include<bits/stdc++.h>
using namespace std;
#define lowbit(i) (i&(-i))
const int N=5e5+10;
long long t[N];
int n,m;
void upd(int i,int w)
{
	for(;i<=n;i+=lowbit(i))
	t[i]+=w;
}//单点修改 
long long qry(int i)
{
	long long res=0;
	for(;i;i-=lowbit(i))
	{
		res+=t[i];
	}
	return res;
}//区间查询 
long long query(int l,int r)
{
	return qry(r)-qry(l-1);
}   
int main()
{
	
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		upd(i,x);
	}
	while(m--)
	{
		int op,x,y;
		scanf("%d%d%d",&op,&x,&y);
		if(op==1) upd(x,y);
		else printf("%lld\n",query(x,y));
	}
	return 0;
}

​

想要做的区间修改的话,可以直接维护差分数列。

对于{a[i]}的差分数列{b[i]=a[i]-a[i-1]},b[i]的前缀和就是a[i]的值,所以对区间[l,r]加上w就可以在b[l]+w,b[r+1]-w,然后用树状数组来维护这个差分数列即可。

二维偏序问题(排序降维,求逆序对)

问题一描述:

求逆序对

前置知识:离散化

#include<bits/stdc++.h>
using namespace std;
int n,a[100];
vector<int> c;
int main()
{
    int n;
    cin>>n; 
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        c.push_back(a[i]);
    }
    sort(c.begin(),c.end());
    c.erase(unique(c.begin(),c.end()),c.end());
    int m=c.size();
    for(int i=1;i<n;i++)
    {
        a[i]=lower_bound(c.begin(),c.end(),a[i])-c.begin()+1;
        cout<<a[i]<<' ';
    }
    return 0;
}

当数据范围比较大,而我们只关系数据相对大小的时候,可以选择使用离散化来查找所想要数据的相对排名。

#include<bits/stdc++.h>
using namespace std;
#define lowbit(i) (i&(-i))
int n,m,a[100],t[100];
void upd(int i,int w)
{
	for(;i<=m;i+=lowbit(i))
	t[i]+=w;
}//单点修改 
int qry(int i)
{
	long long res=0;
	for(;i;i-=lowbit(i))
	{
		res+=t[i];
	}
	return res;
}//区间查询 
vector<int> c;
int main()
{
    int n;
    cin>>n; 
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        c.push_back(a[i]);
    }
    sort(c.begin(),c.end());
    c.erase(unique(c.begin(),c.end()),c.end());
    m=c.size();
    int ans=0;
    cout<<m<<endl;
    for(int i=1;i<=n;i++)
    {
        a[i]=lower_bound(c.begin(),c.end(),a[i])-c.begin()+1;
        int t=qry(m)-qry(a[i]);//前缀和思想
        ans+=t;
        printf("%d : %d\n",i,t);
        upd(a[i],1);
    }
    printf("%d",ans);
    return 0;
}

先把数据映射到排名上,根据排名求出来的逆序对和对原数据求出来的逆序对是一样的。
我们没查询一个位置后对该位置+1表示被标记。然后由于是对数据的顺序标记查询逆序对就是看前m个数和前a[i]个数被标记数组个数的差。(即排名在这个数据之后,但是却先被标记的数字)

问题二描述:

 线段树

 线段树直观图例

其本质是利用分治思想拆分区间的过程,然后回溯将再将他们相加,所能维护的信息需要满足结合律(毕竟最后的代码都是将内容合并)
建树代码

void build(int x,int l,int r) 
{
	if(l==r)
	{
		st[x]=a[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(lx,l,mid),build(rx,mid+1,r);
	st[x]=st[lx]+st[rx];//常用的后续思想,递归计算完后面的值之后在用来补充前面的内容 
}//建树 

区间查询

而查询就是对想要查询的区间不断二分拆解,最后所查询到的区间包含在想要查询的区间内的时候,就返回查询的值,本质上来说就是将查询到的子区间拼接成我们想要查询的区间。

查询代码

int query(int x,int l,int r,int sl,int sr)
{
	if(sl<=l&&sr>=r)
	{
		return st[x];
	}
	int mid=l+r<<1,tmp=0;
	if(sl<=mid) tmp+=query(lx,l,mid,sl,sr);
	if(sr>mid) tmp+=query(rx,mid+1,r,sl,sr);
	return tmp;
}//查询区间[l,r]的合。复杂度为O(log(sr-sl+1))

区间修改

对于区间修改,需要引入“慵懒标记”这个概念,如果遍历修改区间的每一个数字,那么复杂度其实和遍历一整颗树差不多(向下二分了还得回来,感觉还不如直接遍历区间修改),但是我们可以直接对想要的区间进行修改,并对它进行标记,当我们想要利用到接下来的子区间的时候我们再将标记下放,这样就可以O(1)修改区间了

up(标记代码)

void up(int x,int l,int r,int w)
{
	st[x]+=(r-l+1)*w;
	tg[x]+=w;
}

pushdown(下放标记)

void pushdown(int x,int l,int r)
{
	if(!tg[x])return ;
	int mid=l+r>>1;
	up(lx,1,mid,tg[x]),up(rx,mid+1,r,tg[x]);
	tg[x]=0;//避免重复贡献 
}

之后的查询就需要下放标记

int query(int x,int l,int r,int sl,int sr)
{
	if(sl<=l&&sr>=r)
	{
		return st[x];
	}
	pushdown(x,l,r);
	int mid=l+r<<1,tmp=0;
	if(sl<=mid) tmp+=query(lx,l,mid,sl,sr);
	if(sr>mid) tmp+=query(rx,mid+1,r,sl,sr);
	return tmp;
}//查询区间[l,r]的合。复杂度为O(log(sr-sl+1))

 区间修改

void update(int x,int l,int r,int sl,int st,int w)//区间[sl,sr]+w 
{
	if(sl<=l&&r<=sr)
	{
		up(x,l,r,w);
		return ;
	}
	pushdown(x,l,r);
	int mid=l+r>>1,tmp=0;
	if(sl<=mid) update(lx,l,mid,sl,sr,w);
	if(sr>mid) update(rx,mid+1,r,sl,sr,w);
	st[x]=st[lx]+st[rx];
	
}

 动态开点线段树(用来解决区间比较离散的问题)
权值线段树(维护权值出现的次数)

单点增加,单点删除,单点查询

二分线段树,查询排名为n的线段树,查询前驱(小于x最大的数),后继(大于x最小的数)

可持久化线段树(静态区间第k大)

树状数组图片来源

线段树图片来源

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值