ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

论RMQ

2022-07-16 15:32:36  阅读:170  来源: 互联网

标签:RMQ int 最大值 father deep 倍增 节点


啥是倍增思想?

倍增,每次将范围扩大或减少一倍而达到加速的效果

举个栗子,你想要跳到15米远的地方,你怎么找到这个15这个地方,一步一步跳吗,利用倍增的话

预设一个k使2^k>15值 ,这里我们假设k=5,

2^5=32 >15 k- -; k=4; 跳过了,不跳

2^4=16 >15 k- -; k=3; 跳过了,不跳

2^3=8 <=15 n=15-8=7; k- -; k=2; 没跳到,可以跳

2^2=4 <= 7 n=7-4=3 ; k- - ; k=1;没跳到,可以跳

2^1=2 <= 3 n=3-2=1 ; k- - ; k=0;没跳到,可以跳

2^0=1 <= 1 n=1-1=0; 跳到了,停

这样我们只跳了4次,与朴素的15次相比,优化了11次!而且随着数据越来越大,这就是O(n) 和 O(logn)的区别了

因为每一个数都可以由二进制来表示,所以每一个距离我们都可以把他拆分成几个 2^k 相加的形式

觉得倍增很像是二分的逆过程(自底向上的二分),二分是对一个很大的区间折半的查找看当前是否成立,是用一个值去对应区间,利用值不停的缩小区间,而倍增是利用二进制的思想用区间去对应一个值,一个区间一个区间的减少去逼近一个值,虽然没有什么卵用,但是感觉很神奇,你品,你细品

关于倍增思想的总结

兔子跳:很多时候,倍增思想的使用过程就像兔子跳跃的过程一样(设兔子单次跳跃的距离为d,假设在每次跳跃完成后,d=d^2).学习这只兔子的跳跃过程,我们就可以尝试把极高的时间复杂度优化成O(n{log_{2}}^{n})

对数据查询的优化:倍增很多时候是在查询过程中使用的(单次查询的时间复杂度通常为O(1)至 O({log{2}}^{n}).在查询速度优化的同时,往往也需要对数据的预处理.O(n{log{2}}^{n})

图论:在大多数情况下,图论中的点或边都可以按某种方式排序.如果问题的解要求O(NlogN)的时间复杂度,可以考虑倍增思想.在使用倍增思想的情况下,可以结合DP的"最优子结构"和ST表的思想尝试求解.

二进制:在许多情况下,倍增的具体实现与二进制运算有关.在使用倍增思想的过程中,往往可以结合二进制进行考虑.比如:尝试使用位运算优化状态转移.(如:(1<<i)在ST表和倍增求LCA中的应用和(i=i>>1,i&1)在快速幂中的应用)

加速DP状态转移:有时候可以使用倍增的方法优化DP(如:快速幂),使用这种方法可以参考快速幂的实现. (啊其实你也可以不用,等着wa声一片吧)


倍增与LCA

LCA最近公共祖先,相当于在树上找最短路,因为找到了最近公共祖先就相当于找到了最短路

找到u和v第一个不同祖先不同的位置,然后这个位置向上走一步就是最近公共的祖先

但是想找到u,v第一个不同祖先的位置,就要保证u,v在同一深度(才能一起往上移动)

所以这个过程分为三部分,预处理找到每个节点深度,把较深的一点移动到较浅一点的高度,两个一起往上移动直到他们的父亲相同

可以先用一个dfs找到所有节点的深度,用deep数组存下

vector<int>ve[100009];
void dfs(int u)
{
  for(int i=0;i<ve[u].size();i++)
{
  int v=ve[u][i];
  if(vis[v]==0)
      {  
  vis[v]=1;///标记
  deep[v]=deep[u]+1;
  dfs(v);
}
}  
}
找到了深度怎么利用倍增往上移动呢

我们定义father[i][j]为i这个位置往上移动2^j次的祖先是谁

因为i移动2^j次就相当于从i移动2^(j-1)次后再移动2^(j-1)次 找到状态转移方程 father [ i ] [ j ] = father [ father [ i ] [ j -1] ] [ j-1 ] ;

然后利用dp做一个预处理 如下

void st(int n)//倍增预处理i 节点上面1<<j步的节点

{

  for(int j=1; (1<<j)<=n; j++)

  for(int i=1; i<=n; i++)

  father[i][j]=father[father[i][j-1]][j-1];

}

输入边的时候就把father[i][0]初始化好,father[i][0]表示i节点上面2的0次方的祖先

father[1][0]=0表示没有祖先,这个1节点就是根
father[2][0]=1表示2号节点祖先是1
以此内推father[4][0]=2 father[5][0]=2 father[3][0]=1
处理father[4][1]的时候就可以通过 father[ father[4][0]] [ 0 ]得到
father[4][0]=2 所以 father[4][1]=father[2][0]就意味着 i的上面2个节点可以通过i的上面1个节点的再跳1个节点得到 同理4个节点可以跳2次2个节 点得到.


如下


int LCA(int u,int v)
{
if(deep[u]<deep[v])///默认u的深度大
swap(u,v);
int h=deep[u]-deep[v];///求出高度差
for(int i=0; i<20; i++) ///这个操作可以将u节点向上提升任意高度
{
if(h&(1<<i))///二进制位上存在1就提升 1<<i步
{
u=father[u][i];
}
}
if(u==v)///如果u==v表示 u就是v下面的分支节点
return u;
for(int i=19; i>=0; i--) ///找到第一个不相同的节点
{
if(father[u][i]!=father[v][i ])
{
u=father[u][i];
v=father[v][i];
}
}
return father[u][0];///第一个不相同的节点的上一个就是最近公共先
}

通过这种方法,u和v一定能够到达这样一种状态——它们当前 不重合,如果再往上蹦一步,就会重合。所以再往上蹦一步得 到的就是LCA

附洛谷模板题代码:https://www.luogu.com.cn/problem/P3379

include<bits/stdc++.h>

using namespace std;

define ll long long

define lowbit(a) ((a) & -(a))

define clean(a, b) memset(a, b, sizeof(a))

const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 5e5 + 9;
const int Maxn = 29;

int deep[maxn],father[maxn][Maxn];
vector<int>ve[maxn];
int vis[maxn];
void dfs(int now)
{
int len=ve[now].size();
vis[now]=1;
for(int i=0;i<len;i++)
{
int next=ve[now][i];
if(!vis[next])
{
deep[next]=deep[now]+1;
father[next][0]=now;
dfs(next);
}
}
}
void st(int n)///倍增预处理i 节点上面1<<j步的节点
{
for(int j=1; (1<<j)<=n; j++)
for(int i=1; i<=n; i++)
father[i][j]=father[father[i][j-1]][j-1];
}
int LCA(int u,int v)
{
if(deep[u]<deep[v])///默认u的深度大
swap(u,v);
int h=deep[u]-deep[v];///求出高度差
for(int i=0; i<20; i++) ///这个操作可以将u节点向上提升任意高度
{
if(h&(1<<i))///二进制位上存在1就提升 1<<i步
{
u=father[u][i];
}
}
if(u==v)///如果u==v表示 u就是v下面的分支节点
return u;
for(int i=19; i>=0; i--) ///找到第一个不相同的节点
{
if(father[u][i]!=father[v][i])
{
u=father[u][i];
v=father[v][i];
}
}
return father[u][0];///第一个不相同的节点的上一个就是最近公共祖先
}
int main()
{
int n,m,s,u,v,a,b;
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
ve[u].push_back(v);
ve[v].push_back(u);
}
dfs(s);
st(n);
while(m--)
{
scanf("%d%d",&a,&b);
printf("%d\n",LCA(a,b));
}
return 0;
}

倍增与RMQ
RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j之间的最小/大值

这样的问题我们可以用线段树来解决,但代码比较冗长,用ST算法也可以解决这个问题,该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询

ST(Sparse Table)算法是一个非常有名的在线处理RMQ问题的算法,它可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询

预处理 我们定义f[i, j]表示从第i个数起连续2^j个数中的最大值

f[i, j]表示从第i个数起连续2^j个数中的最大值

f[i][j]表示i开始的连续2j 个点的最大值。

则f[i][0]表示i开始连续1个点的最大值即a[i];

f[i][1]表示i开始连续2个点的最大值即a[i]和a[i+1]的最大值;

f[i][2]表示i开始连续4个点的最大值即a[i]~a[i+3]中的最大值;

f[i][3]表示i开始连续8个点的最大值即a[i]~a[i+7]中的最大值;

......

f[i][logn]开始连续n个点的最大值即 a[i]~a[i+n-1];(i+n-1<=n)

我们把f[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从 i 到i + 2 ^ (j - 1) - 1为一段,i + 2 ^ (j - 1)到i + 2 ^ j - 1为一段(长度都为2 ^ (j - 1))

f[i,j]就是这两段各自最大值中的最大值

状态转移方程f[i, j]=max(f[i,j-1], f[i + 2^(j-1),j-1])

void ST(int n)///预处理
{
for(int i=1;i<=n;i++)
f[i][0]=a[i];///初始化
for(int j=1;(1<<j)<=n;j++)///枚举区间长度
{
for(int i=1;i+(1<<j)-1<=n;i++)///枚举起点
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
}

查询
假如我们需要查询的区间为(i,j),那么我们需要找到覆盖这个闭区间(左边界取i,右边界取j)的最小幂 (区间重复并不影响答案,因为我们要找的是最大或最小值)

因为这个区间的长度为j - i + 1,所以我们可以取k=log2( j - i + 1),则有:RMQ(A, i, j)=max{f[i , k], f[ j - 2 ^ k + 1, k]}

 

所以

int RMQ(int l,int r)
{
int k=floor(log2( r-l+1 ));///向下取整
printf("k=%d\n",k);
return max(f[l][k],f[r-(1<<k)+1][k]);
}

 

本解采用某牛犇的发表,非本人创作

标签:RMQ,int,最大值,father,deep,倍增,节点
来源: https://www.cnblogs.com/52liai/p/16480078.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有