开拓计划21/2025集训作业表3 - 倍增&ST表&LCA&次小生成树
倍增&ST表
概念
- Q:倍增是什么?
- A:倍增,顾名思义是成倍增长的意思,它利用了二进制的性质和预处理(俗称打表)的思想,在 \(O(\log n)\) 内完成一些操作。
- Q:ST表是什么?
- A:ST表主要用于解决RMQ(区间最值问题),它用了打表的思想,但是没有将表打完整。
ST表的原理
在实现 RMQ 问题时,我们可以通过打表来提升效率。提前计算出 \([l,r]\) 的答案。但是这样打表就需要 \(O(n^2)\) 实在是太浪费时间了。于是我们发现,表没必要打得这么详细,我们设 \(f_{i,j}\) 表示区间 \([i,i+2^j-1]\) 的最大值。
预处理:
- \(f_{i,0}=a_i\)
- \(f_{i,j}=\max(f_{i,j-1},f_{i+2^{j-1},j-1})\)
- 时间复杂度 \(O(n \log n)\)
查询:
- 如果要查询区间 \([l,r]\),先找 \(2^j \le r-l+1\) 中 \(j\) 的最大值。
\(\therefore j=\lfloor \log_2(r-l+1) \rfloor\) - \(ans=\max(f_{l,j},f_{r-2^j+1,j})\)
- 时间复杂度 \(O(n)\)
ST表的代码
#include<bits/stdc++.h>
using namespace std;
const int N=25005;
int f[N][35];
int main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);int n,q;cin>>n>>q;for(int i=1;i<=n;i++) cin>>f[i][0];int mx=log2(n);//预处理for(int j=1;j<=mx;j++){int i,k=(1<<j-1);for(i=1;i<=n-k;i++) f[i][j]=min(f[i][j-1],f[i+k][j-1]);for(;i<=n;i++) f[i][j]=f[i][j-1];}//查询for(int i=1;i<=q;i++){int l,r;cin>>l>>r;int j=log2(r-l+1);cout<<min(f[l][j],f[r-(1<<j)+1][j])<<"\n";}return 0;
}
LCA
概念
- Q:LCA 是什么?
- A:LCA是指树上最近公共祖先,如下图所示,\(7\) 和 \(5\) 的 LCA 就是\(2\)。
求法
倍增法
还是要求 \(7\) 和 \(5\) 的LCA。
设 \(f_{i,j}\) 表示第 \(i\) 个节点,跳 \(2^j\) 步的时候跳到的位置,\(x\) 和 \(y\) 分别为 \(5\) 和 \(7\) 的指针。
分为两个阶段:
- 第一阶段:将 \(7\) 和 \(5\) 的指针调整到同一高度。
- 通过已经预处理的 \(f\) 数组开始尝试跳,如果高度大了就是条过头了,回去重新跳,直到 \(x\) 的新高度小于 \(y\),就往上跳,跳完了再继续跳,直到高度一样。
- 第二阶段
- 两个指针同时开始向上跳,跳过头了就回来,直到跳到同一个点。
倍增求LCA的代码
void dfs(int x,int fa){dep[x]=dep[fa]+1;f[x][0]=fa;for(int j=1;;j++){f[x][j]=f[f[x][j-1]][j-1];if(f[x][j]==0){mx=max(mx,j-1);break;}}for(auto i:vt[x]){if(i!=fa) dfs(i,x);}
}
int Lca(int x,int y){if(dep[x]<dep[y]) swap(x,y);for(int j=mx;j>=0;j--){if(dep[f[x][j]]>=dep[y]) x=f[x][j];}if(x==y) return x;for(int j=mx;j>=0;j--){if(f[x][j]!=f[y][j]) x=f[x][j],y=f[y][j];}return f[x][0];
}
次小生成树
非严格次小生成树
思路:
- 先用
kurskal
求最小生成树。 - 枚举最小生成树以外的边,将每一条边分别加入再减掉原来的最大边。
- 求所有结果的最小值。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100005;
int dep[N],f[N][35],fa[N],used[N<<1],dis[N][35];
int n,m,Ans=0;
struct node{int s,e,w;}edge[N<<1];
struct nodf{int v,e;};
bool cmp(node a,node b){return a.w<b.w;}
vector<nodf> vt[N];
void dfs(int x,int fa){for(int j=1;(1<<j)<=dep[x];j++){f[x][j]=f[f[x][j-1]][j-1];dis[x][j]=max(dis[f[x][j-1]][j-1],dis[x][j-1]);}for(auto i:vt[x]){if(i.v!=fa){dep[i.v]=dep[x]+1;f[i.v][0]=x;dis[i.v][0]=i.e;dfs(i.v,x);}}
}
int Lca(int x,int y){int mx=0;if(dep[x]<dep[y]) swap(x,y);for(int j=30;j>=0;j--){if(dep[x]-(1<<j)>=dep[y]){mx=max(dis[x][j],mx);x=f[x][j];}}if(x==y) return mx;for(int j=30;j>=0;j--){if(f[x][j]!=f[y][j]){mx=max(dis[x][j],mx);mx=max(dis[y][j],mx);x=f[x][j],y=f[y][j];}}return max(mx,max(dis[x][0],dis[y][0]));
}
int getf(int x){if(x==fa[x]) return x;return fa[x]=getf(fa[x]);
}
bool kurskal(){int cnt=0;for(int i=1;i<=m;i++){if(cnt==n-1) return 0;if(getf(edge[i].s)!=getf(edge[i].e)){cnt++;fa[getf(edge[i].s)]=getf(edge[i].e);used[i]=1;Ans+=edge[i].w;vt[edge[i].s].push_back({edge[i].e,edge[i].w});vt[edge[i].e].push_back({edge[i].s,edge[i].w});}}return 1;
}
signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++) fa[i]=i;for(int i=1;i<=m;i++) cin>>edge[i].s>>edge[i].e>>edge[i].w;sort(edge+1,edge+1+m,cmp);if(kurskal()){cout<<"Keng Die!";return 0;}dep[1]=1;dfs(1,0);int ans=0x7fffffff;for(int i=1;i<=m;i++){if(used[i]==0){ans=min(ans,Ans+edge[i].w-Lca(edge[i].s,edge[i].e));}}cout<<ans;return 0;
}
严格次小生成树
思路
同非严格只不过要同时维护次大值,因为相等的时候就需要换成次大值。
代码
oi wiki