\(f[i][j]\)表示从\(i\)开始的长度为\(2^{j}\)的区间(即区间\([i, i+2^{j}-1]\))算法
递推公式(j在外层递增):优化
\(f[i][j]=max\{f[i][j-1], f[i+2^{j-1}][j-1]\}\)spa
即将区间\([l, r]\)分为两个区间合并code
分为两段,第一段为区间\([l, 2^k]\),第二段为区间\([r-2^k+1, r]\),其中\(k\)为知足\(2^{k}\le r-l+1\)的全部数中最大的那个数get
即区间\([l,r]\)的最大值为\(max\{f[l][k], f[r-2^{k}+1][k]\}\)io
忠诚 洛谷 P1816class
屡次查询区间最小值效率
#include <cstdio> #define MAXN 100010 #define MIN(A,B) ((A)<(B)?(A):(B)) using namespace std; int f[MAXN][20]; int getk(int x){ int cnt=0,cur=1; while(cur<=x){ ++cnt; cur*=2; } return cnt-1; } int main() { int m,n; scanf("%d %d", &m, &n); for(register int i=1;i<=m;++i) scanf("%d", &f[i][0]); int mx_len = getk(m); for(register int len=1;len<=mx_len;++len) for(register int i=1;i+(1<<len)-1<=m;++i) f[i][len]=MIN(f[i][len-1], f[i+(1<<(len-1))][len-1]); while(n--){ int l, r; scanf("%d %d", &l, &r); int k=getk(r-l+1); printf("%d ", MIN(f[l][k], f[r-(1<<k)+1][k])); } return 0; }
首先结合dfs预处理出\(f[i][j]\),\(f[i][j]\)表示节点\(i\)向上跳\(2^{j}\)层的节点二进制
递推公式:方法
\(f[i][j]=f[f[i][j-1]][j-1]\)
即节点\(i\)分两次向上跳,每次跳\(2^{j-1}\)层跳到的节点就是节点\(i\)向上跳\(2^{j}\)层的节点(\(2^{j-1}\times 2=2^{j}\))
void load(int x, int fa){ f[x][0]=fa; dep[x]=dep[fa]+1; for(int i=1;i<20;++i) f[x][i]=f[f[x][i-1]][i-1]; } void dfs(int u, int fa){ load(u, fa); for(int i=head[u];i;i=nxt[i]){ int v=vv[i]; if(v==fa) continue; dfs(v, u); } }
同时也能够像下面预处理出\(log^{n}_{2}\)的全部值以优化常数
for(int i=2;i<=tot;++i) lg2[i]=lg2[i>>1]+1; // 预处理log2n
首先使两个查询节点跳至同一高度后(由于它们的最近公共祖先不可能低于这两点,跳跃方法同下),当前层记为\(x\),而后从\(log^{x}_{2}\)到0枚举(递减能保证能够彻底分解成二进制)\(j\),若是上跳\(2^{j}\)层后不重合,那么就继续跳,重合则不跳,使两点层数一直逼近最近公共祖先,最后跳完\(2^{0}\)层后,两点一定会停在最近公共祖先的下一层,因此最后直接取当前层\(i\)的\(f[i][0]\)就行了。
其中,能够直接从最大可能的\(i\)开始枚举,由于反正\(i\)也很小。
inline int lca(int a, int b){ if(dep[a]<dep[b]) swap(a, b); for(int i=20;i>=0;--i) if(dep[f[a][i]]>=dep[b]) a=f[a][i]; if(a==b) return a; for(int i=20;i>=0;--i) if(f[a][i]!=f[b][i]) a=f[a][i], b=f[b][i]; return f[a][0]; }
这是一种在线求\(LCA\)的算法,其实还有\(Tarjan\)这种效率高的离线算法。
另外还有一种\(nlogn\)预处理,每次\(O(1)\)查询的方法。
即用欧拉序+RMQ可实现\(O(1)\)求得LCA。先dfs全树,记录欧拉序(就是记下\(dfs\)走过的节点),而后在欧拉序上以节点深度做为权值建ST表,每次查欧拉序上深度最小的点即为LCA。此时将问题转换为区间求最小值RMQ问题。
void dfs(int u, int fa){ dfn[u]=++tot; f[tot][0]=u; dep[u]=dep[fa]+1; for(int i=head[u];i;i=nxt[i]){ int v=vv[i]; if(v==fa) continue; dfs(v, u); f[++tot][0]=u; // 再记录 } }
for(int i=1;i<20;++i) for(int j=1;j+(1<<i)-1<=tot;++j) if(dep[f[j][i-1]]<dep[f[j+(1<<(i-1))][i-1]]) f[j][i]=f[j][i-1]; else f[j][i]=f[j+(1<<(i-1))][i-1]; for(int i=2;i<=tot;++i) lg2[i]=lg2[i>>1]+1; // 预处理log2n
int lca(int a, int b){ int l=dfn[a],r=dfn[b]; if(l>r) swap(l,r); int lg=lg2[r-l+1]; if(dep[f[l][lg]]<dep[f[r-(1<<lg)+1][lg]]) return f[l][lg]; else return f[r-(1<<lg)+1][lg]; }
求树上两点\(a,b\)路径上的最小权值
咱们再设一个\(g[i][j]\)表示节点\(i\)向上跳\(2^j\)内所通过的最小权值便可,转移方程:
\(g[i][j]=min(g[i][j-1], g[f[i][j-1]][j-1])\)