区间信息维护与查询

倍增、ST、RMQ

RMQ(区间最值查询)问题有多种解决方法,用线段树和ST解决RMQ问题对比如下:

  • 线段树预处理的时间为O(nlogn),查询的时间为O(logn),支持在线修改;
  • ST预处理的时间为O(nlogn),查询的时间为O(1),不支持在线修改。

ST(sparse table,稀疏表)算法采用了倍增思想,在O(nlogn)时间构造了一个二维表之后,可以在O(1)时间在线查询[l,r]区间的最值,有效解决在线RMQ(range minimum/maximum query,区间最值查询)问题。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1001;
int f[maxn][21];//f[i][j]记录从i开始长度为2^j的区间最值 
int a[maxn];
int n;
int lb[maxn];
void st_creat()//ST表建立 
{
	for(int i=1;i<=n;i++)
		f[i][0]=a[i];
	int k=log2(n);
	for(int j=1;j<=k;j++)
	{
		for(int i=1;i<=n-(1<<j)+1;i++)
		{
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
		}
	}
}
int st_query(int l,int r)//ST表查询 
{
	int k=log2(r-l+1);
	return max(f[l][k],f[r-(1<<k)+1][k]);
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	st_creat();
	while(1)
	{
		int l,r;
		cin>>l>>r;
		cout<<st_query(l,r);
	}
}

也可为提前计算好所有可能的log值。算法如下:

找到log(i-1)和log(i)之间的关系:

若i是2的某次幂:如i=8,二进制的1000,则i&(i-1)等于0;

否则:i&(i-1)不等于0;

void initlog()
{
	lb[0]=-1;
	for(int i=1;i<maxn;i++)
		lb[i]=(i&(i-1))?lb[i-1]:lb[i-1];
} 

最近公共祖先LCA

求解LCA的方法有很多,包括暴力搜索法、树上倍增法、在线RMQ算法、离线Tarjan算法和树链剖分

  • 在线算法:以序列化方式一个一个地处理输入,也就是说,在开始时并不需要知道所有输入,在解决一个问题后立即输出结果。
  • 离线算法:在开始时已知问题地所有输入数据,可以一次性回答所有问题。

暴力搜索法

有两种:

  1. 向上标记法:从u节点一直向上走到根节点,标记所有经过的节点,若能标记v,则v=LCA(u,v);否则v也向上走,第一次遇到已标记的节点则为LCA(u,v)。
  2. 同步前进法:将u,v中较深的节点一直向上走到和另一节点相同深度,二者再同时向上推进,直到走到同一个节点,该点就是 LCA(u,v);若较深的节点u到达另一节点的同一深度时,所在节点就是v,则v=LCA(u,v)。

以暴力法求解LCA,时间复杂度最差都为O(n)。 

树上倍增法

f[i][j]表示i的2^j个祖先,如f[u][0]表示u的父节点,f[u][1]表示u的父节点的父节点,若f[i][j]超过根节点,则赋值为0.

递推公式:f[i][j]=f[f[i][j-1]][j-1]

算法思路:

首先让深度较深的节点向上走到与另一节点同一深度,只不过按倍增思想走。

然后到达同一深度的两节点一起向上走,也是采用倍增思想。

void st_creat()
{
	for(int j=1;j<=k;j++)
		for(int i=1;i<=n;i++)
			f[i][j]=f[f[i][j-1]][j-1];
}
int LCA(int x,int y)
{
	if(d[x]>d[y])//保证x的深度小于等于y 
		swap(x,y);
	for(int i=k;i>=0;i--)
	{
		if(d[f[y][i]]>=d[x])//y向上走到与x同一深度 
			y=f[y][i];
	}
	if(x==y)
		return x;
	for(int i=k;i>=0;i--)
		if(f[x][i]!=f[y][i])//x,y一起向上走 
		{
			x=f[x][i];
			y=f[y][i];
		}
	return f[x][0];
}

算法分析:

创建ST需要O(nlogn)时间,每次查询需要O(logn)时间,一次建表,多次使用,若只有几次查询,还不如暴力搜索快。

在线RMQ算法

欧拉序列指在深度遍历过程中把经过的节点一个个记录下来,把回溯时的节点也记录下来。

两个节点的LCA一定是两个节点之间欧拉序列中深度最小的节点,寻找深度最小时可以使用RMQ算法

void dfs(int u,int d)
{
	vis[u]=true;
	pos[u]=++tot;//u首次出现的下标 
	seq[tot]=u;//dfs遍历得到的欧拉序列 
	dep[tot]=d;//深度 
	for(int i=head[i];i;i=e[i].next)
	{
		int v=e[i].to;
		int w=e[i].c;
		if(vis[v])
			continue;
		dist[v]=dis[u]+w;
		dfs(v,d+1);
		seq[++tot]=u;
		deq[tot]=d;
	}	
} 
void st_creat()
{
	for(int i=1;i<=tot;i++)//f[i][j]表示从i开始长度为2^j的序列里深度最小的节点的下标 
		f[i][0]=i;
	int k=log2(tot);
	for(int j=1;j<=k;j++)
		for(int i=1;i<=tot-(1<<j)+1;i++)
			if(dep[f[i][j-1]]<dep[f[i+(1<<(j-1))][j-1]])
				f[i][j]=f[i][j-1];
			else
				f[i][j]=f[i+(1<<(j-1))][j-1];
} 
int rmq_query(int l,int r)//查询[l,r]的区间最值 
{
	int k=log2(r-l+1);
	if(dep[f[l][k]]<dep[f[r-(1<<k)+1][k]])
		return f[l][k];
	else
		return f[r-(1<<k)+1][k];
} 
int LCA(int x,int y)//求x,y的最近公共祖先 
{
	int l=pos[x];//首次出现的下标 
	int r=pos[y];
	if(l>r)
		swap(l,r);
	return seq[rmq_query(l,r)];
}

算法分析:

在线RMQ算法是基于倍增和RMQ的动态规划算法,其预处理包括深度遍历和创建ST,需要

O(nlogn)时间,每次查询需要O(1)时间。

注意:虽然都用到了ST,但在线RMQ算法中表示区间最值,树上倍增算法中的ST表示向上走的步数

Tarjan算法

这里的Tarjan算法是用于解决LCA问题的离线算法,利用并查集优越的时空复杂性,可以在O(n+m)时间内解决LCA问题。

  1. 初始化集合号数组和访问数组,fa[i]=i,vis[i]=0;
  2. 从u出发深度优先遍历,标记vis[u]=1,深度优先遍历u所有未被访问的邻接点,在遍历过程中更新距离,回退是更新集合号。
  3. 当u的邻接点全部遍历完毕时,检查关于u的所有查询,若存在一个查询u,v,而vis[v]==1,则利用并查集查找v的祖宗,找到的节点就是u,v的最近公共祖先。

int find(int x)
{
	return x!=fa[x]?fa=find(fa[x]):x;
}
void tarjan(int u)
{
	vis[u]=1;
	for(int i=head[u];i;i=e[i].next)
	{
		int v=e[i].to;
		int w=e[i].c;
		if(vis[v])
			continue;
		dis[v]=dis[u]+w;
		tarjan(v);
		fa[v]=u;
	}
	for(int i=0;i<query[u].size();i++)
	{
		int v=query[u][i];
		int id=query_id[u][i];
		if(vis[y])
		{
			int lca=find(v);
			ans[id]=dis[u]+dis[v]-2*dis[lca];
		}
	}
}

树状数组

一维树状数组

树状数组又称二进制索引树(Binary Indexed Trees),修改和查询的时间复杂度均为O(logn),设原数组为a[i],树状数组为c[i],则:

区间长度

c[i]存储的区间长度是2^i的二进制表示下末尾0的个数次幂,如c[6],6二进制是110,末尾有1个0,则c[6]存储长度为2^1,即c[6]=a[5]+a[6]。同理c[5]=a[5],而这个长度其实就是最后一个1和剩下的0,怎么去获得这个数呢?

如i=20,二进制为10100,对它取反为01011,加一为01100,将它与原数相与即可得100,也就是4.

在计算机中二进制数采用的是补码表示,-i的补码正好是i取反加1,因此(-i)&i就是区间的长度。

int lowbit(int i)
{
	return (-i)&i;
}

前驱和后继

  1. 直接前驱:c[i]的直接前驱为c[i-lowbit(i)],即c[i]左侧紧邻的子树的根。
  2. 直接后继:c[i]的直接后继为c[i+lowbit(i)],即c[i]的父节点。

可利用直接前驱和后继一步步推出所有前驱和后继。

查询前缀和

前i个元素的前缀和sum[i]等于c[i]加上c[i]的前驱

int sum(int i)
{
	int s=0;
	for(;i>0;i-=lowbit(i))
		s+=c[i];
	return s;
}

点更新

若对a[i]进行修改,令a[i]加上一个数z,则只需要更新c[i]及其后继(祖先),另这些节点都加上z即可.

void add(int i,int z)
{
	for(;i<=n;i+=lowbit(i))
		c[i]+=z;
}

注意:树状数组的下标从1开始,不可以从0开始,因为lowbit(0)=0时会出现死循环。

查询区间和

int sum(int i,int j)
{
	return sum(j)-sum(i-1);
}

多维树状数组

二维数组a[n][n],树状数组c[][]的查询和修改方法如下:

查询前缀和

二维数组的前缀和实际上是从数组左上角到当前位置(x,y)矩阵的区间和,在一位数组查询前缀和的代码上加上一层循环即可。

int sum(int x,int y)
{
	int s=0;
	for(int i=x;i>0;i-=lowbit(i))
		for(int j=y;j>0;j-=lowbit(j))
			s+=c[i][j];
	return s;
}

更新

也是加一层循环即可。

void add(int x,int y,int z)
{
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
			c[i][j]+=z;
}

查询区间和值

int sum(int x1,int y1,int x2,int y2)
{
	return sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1);
}

树状数组的局限性

树状数组主要用于查询前缀和、区间和及点更新,对点查询、区间修改效率较低

  1. 前缀和查询:普通数组需要O(n)时间,树状数组需要O(logn)时间
  2. 区间和查询:普通数组需要O(n)时间,树状数组需要O(logn)时间
  3. 点更新:普通数组需要O(1)时间,树状数组需要O(logn)时间
  4. 点查询:普通数组需要O(1)时间,树状数组需要O(logn)时间
  5. 区间修改:普通数组需要O(n)时间,树状数组需要O(n logn)时间
  6. 减法规则:当问题满足减法规则时,如求从a[i]到a[j]的区间和,可以用sum[j]-sum[i-1],但问题不满足减法规则,如求区间a[i]到a[j]的最大值,就不可以用树状数组,此时可以用线段树解决。

线段树 

线段树是一种基于分治思想的二叉树,它的每一个节点都对应一个[L,R]区间,叶子节点的l和r相等,线段树除了最后一层,其他层构成一颗满二叉树。

对于区间最值问题,线段树的每个节点都包含三个域:l,r,mx。其中l和r分别表示区间的左右端点,mx表示区间[l,r]的最值。

线段树的构建:

void build(int k,int l,int r)//创建线段树,节点的存储下标为k,节点的区间为[l,r] 
{
	tree[k].l=l;
	tree[r].r=r;
	if(l==r)
	{
		tree[k].mx=a[l];//scanf("%d",&tree[k].mx);
		return;
	}
	int mid,lc,rc;
	mid=(l+r)/2;//划分点 
	lc=k*2;//左子节点的存储下标 
	rc=k*2+1;//右子节点的存储下标 
	build(lc,l,mid);
	build(rc,mid+1,r);
	tree[k].mx=max(tree[lc].mx,tree[rc].mx);//节点的最大值等于左右子节点最值的最值 
}

点更新

采用递归进行点更新:

  1. 若是叶子节点则修改该节点的最值
  2. 若是非叶子节点,则判断是在左子树中更新还是在右子树中更新
  3. 返回时更新节点的最值

void update(int k,int i,int v)//将a[i]更新为v
{
	if(tree[k].l==tree[k].r && tree[k].l==i)
	{
		tree[k].mx=v;
		return;
	} 
	int mid,lc,rc;
	mid=(tree[k].l+tree[k].r)/2;
	lc=k*2;
	rc=k*2+1;
	if(i<=mid)
		update(lc,i,v);
	else
		update(rc,i,v);
	tree[k].mx=max(tree[lc].mx,tree[rc].mx);
} 

区间查询

查询一个[l,r]区间的最值。

  1. 若节点所在的区间被查询区间[l,r]覆盖,则返回该节点的最值。
  2. 判断是在左子树中查询,还是在右子树中查询。
  3. 返回最值。
int query(int k,int l,int r)//求[l,r]区间的最大值 
{
	if(tree[k].l>=l && tree[k].r<=r)
		return tree[k].mx;
	int mid,lc,rc;
	mid(tree[k].l+tree[k].r)/2;
	lc=k*2;
	rc=k*2+1;
	int Max=-inf;//注意:局部变量可以,全局变量不可以 
	if(l<=mid)
		Max=max(Max,query(lc,l,r));
	if(r>mid)
		Max=max(Max,query(rc,l,r));
	return Max;
}

 线段树中的“懒操作”

若对区间的所有点都进行更新,不可以对其中每个点更新,复杂度过高,可以引入懒操作。

区间更新

  1. 若当前节点的区间被查询区间[l,r]覆盖,则仅对该节点进行更新并做懒标记,表示该节点已被更新,对该节点的子节点暂不更新。
  2. 判断是在左子树中查询还是在右子树中查询。在查询过程中,若当前节点带有懒标记,则将懒标记下传给子节点(将当前节点的懒标记清除,将子节点更新并做懒标记),继续查询。
  3. 在返回时更新最值。

void lazy(int k,int v)
{
	tree[k].mx=v;//更新最值 
	tree[k].lz=v;//做懒标记 
}
void pushdown(int k)//向下传递懒标记 
{
	lazy(2*k,tree[k].lz);
	lazy(2*k+1,tree[k].lz);
	tree[k].lz=-1;//清除自己的懒标记 
}
void update(int k,int l,int r,int v)//将[l,r]区间的所有元素都更新为v 
{
	if(tree[k].l>=l && tree[k].r<=r)
		return lazy(k,v);
	if(tree[k].lz!=-1)
		pushdown(k);
	int mid,lc,rc;
	mid=(tree[k].l+tree[k].r)/2;
	lc=2*k;
	rc=2*k+1;
	if(l<=mid)
		update(lc,l,r,v);
	if(r>mid)
		update(rc,l,r,v);
	tree[k].mx=max(tree[lc].mx,tree[rc].mx);
}

区间查询

带有懒标记的区间查询,在查询中若遇到节点有懒标记,则下传懒标记,继续查询。

int query(int k,int l,int r)
{
	int Max=-inf;
	if(tree[k].l>=l && tree[k].r<=r)
		return tree[k].mx;
	if(tree[k].lz!=-1)
		pushdown(k);
	int mid,lc,rc;
	mid=(tree[k].l+tree[k].r)/2;
	lc=2*k;
	rc=2*k+1;
	if(l<=mid)
		Max=max(Max,query(lc,l,r));
	if(r>mid)
		Max=max(Max,query(rc,l,r));
	return Max;
} 

分块

树状数组和线段树维护的信息必须满足信息合并特性(如区间可加、可减),而分块算法作为优化的暴力算法,几乎可以解决所有区间更新和区间查询问题。

分块算法通常将块的大小设为根号n,每一块都有sqrt(n)个元素,最后一块可能少于它,用pos[i]表示第i个位置所属的块。

单点更新:一般先将对应块的懒标记下传,再暴力更新块的状态,时间复杂度为O(sqrt(n));

区间更新:若区间更新横跨若干块,则只需对完全覆盖的块打上懒标记,最多需要修改两端的两个块,对两端剩余的部分暴力更新块的状态。每次更新都最多遍历sqrt(n)个块,遍历每个块的时间复杂度为O(1),两端两个块暴力更新sqrt(n)次,总时间复杂度为O(sqrt(n));

区间查询:与区间更新类似,“大段维护,局部朴素”。

t=sqrt(n*1.0);
int num=n/t;
if(n%t)
	num++;
for(int i=1;i<=num;i++)
{
	L[i]=(i-1)*t+1;
	R[i]=i*t;
}
R[num]=n;
for(int i=1;i<=num;i++)
	for(int j=L[i];j<=R[i];j++)
	{
		pos[j]=i;//表示属于哪个块 
		sum[i]+=a[j];//计算每块的和值 
	}
void change(int l,int r,long long d)//[l,r]区间的元素加d
{
	int p=pos[l];
	int q=pos[r];
	if(p==q)
	{
		for(int i=l;i<=r;i++)
			a[i]+=d;
		sum[p]+=d*(r-l+1);
	}
	else
	{
		for(int i=p+1;i<=q-1;i++)//对中间完全覆盖的块打上懒标记 
			add[i]+=d;
		for(int i=l;i<=R[p];i++)
			a[i]+=d;
		sum[p]+=d*(R[p]-l+1);
		for(int i=L[q];i<=r;i++)
			a[i]+=d;
		sum[q]+=d*(r-L[q]+1);
	}
} 
long long ask(int l,int r)//区间查询 
{
	int p=pos[l];
	int q=pos[r];
	long long ans=0;
	if(p==q)
	{
		for(int i=l;i<=r;i++)
			ans+=a[i];
		ans+=add[p]*(r-l+1);
	}
	else
	{
		for(int i=p+1;i<=q-1;i++)// 
			ans+=sum[i]+add[i]*(R[i]-L[i]+1);
		for(int i=l;i<=R[p];i++)
			ans+=a[i];
		ans+=add[p]*(R[p]-l+1);
		for(int i=L[q];i<=r;i++)
			ans+=a[i];
		ans+=add[q]*(r-L[q]+1);
	}
	return ans;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值