标签:lca str 拆分 调整 点距 ahoi2008 last tps 基本
\(LCA\) 即 最近公共祖先,定义如下:
给定一颗有根树,若节点 \(z\) 既是节点 \(x\) 的祖先,又是节点 \(y\) 的祖先,则称之为 \(x,y\) 的公共祖先。
在节点 \(x,y\) 所有的公共祖先中深度最大的即为最近公共祖先,记为 \(LCA(x,y)\)。
\(LCA\) 的主要作用是求树上两点之间的路径,同时也有一些别的 玄学 用处。
其实就是暴力方法。
从 \(x\) 向根节点走,并标记所有走过的点。
从 \(y\) 向根节点走,当遇到已标记的第一个点时,这个点就是 \(LCA(x,y)\)。
对于这种方法,最坏时间复杂度为 \(O(n)\)。
这是一个很重要的算法,除了求 \(LCA\) 之外,还有很大的用处。
设 \(F[x,k]\) 表示 \(x\) 的 \(2^k\) 辈祖先,特别的若该点不存在就令 \(F[x,k]=0\),而 \(F[x,0]\) 自然就是 \(x\) 的父亲节点。
除此之外 \(F[x,k]=f[f[x,k-1],k-1]\)(类似于动态规划),这就是这个算法的核心。
以上预处理部分,代码实现时用一遍 \(dfs\) 搞定,时间复杂度为 \(O(n\ log\ n)\)。
你会发现它并没有上面的 \(O(n)\) 优,因为这并不是它快速的原因,它的快体现在查询上。
当求 \(LCA(x,y)\) 时主要分为以下几步:
多次查询的时间复杂度为 \(O((n+m)log\ n)\)。相比上面的 \(O(nm)\) 确实有所优化。
核心代码如下:
void dfs(int x,int fa){
d[x]=d[fa]+1;
f[x][0]=fa;
for(int i=1;(1<<i)<=d[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].to!=fa) dfs(edge[i].to,x);
}
return;
}
//以上是预处理
int LCA(int x,int y){
if(d[x]>d[y]) swap(x,y);
for(int i=20;i>=0;i--)
if(d[f[y][i]]>=d[x]) y=f[y][i];
if(x==y) return x;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
这部分待更。
既然我们已经学会了最最最基本的 \(LCA\),就用几道例题来加深理解吧。
将 最大生成树 与 \(LCA\) 完美结合的问题。
首先货车司机一定是走最大的道路,那么连接一个点与所有其它点的两线的最大值显然只有一条,
那么这就构成了一颗 最大生成树,做法和 最小生成树 一样就好了,只是 \(Kruscal\) 排序时从大到小排。
然后两点之间的路径用 \(LCA\) 维护即可。
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define N 10010
#define M 50010
#define INF 999999999
using namespace std;
int n,m,fa[N][30],w[N][30],depth[N];
int p[N],lg[N];
int head[N],cnt=0;
bool vis[N];
struct node1{
int x,y,dis;
}a[M];
struct node2{
int next,to,val;
}edge[N*2];
bool cmp(node1 a,node1 b){
return a.dis>b.dis;
}
int find(int x){
if(x==p[x]) return x;
return p[x]=find(p[x]);
}
void addedge(int x,int y,int z){
cnt++;
edge[cnt].next=head[x];
edge[cnt].to=y;
edge[cnt].val=z;
head[x]=cnt;
return;
}
void kruscal(){
sort(a+1,a+m+1,cmp);
for(int i=1;i<=n;i++) p[i]=i;
for(int i=1;i<=m;i++){
int fx=find(a[i].x);
int fy=find(a[i].y);
if(fx==fy) continue;
p[fx]=fy;
addedge(a[i].x,a[i].y,a[i].dis);
addedge(a[i].y,a[i].x,a[i].dis);
}
return;
}
void dfs(int x,int last){
vis[x]=true;
for(int i=head[x];i;i=edge[i].next){
int y=edge[i].to;
if(y==last) continue;
depth[y]=depth[x]+1;
fa[y][0]=x;
w[y][0]=edge[i].val;
dfs(y,x);
}
return;
}
void pre_LCA(){
for(int i=1;i<=n;i++){
if(!vis[i]){
dfs(i,0);
fa[i][0]=i;
w[i][0]=INF;
}
}
for(int i=1;i<=20;i++)
for(int j=1;j<=n;j++){
fa[j][i]=fa[fa[j][i-1]][i-1];
w[j][i]=min(w[j][i-1],w[fa[j][i-1]][i-1]);
}
return;
}
int LCA(int x,int y){
if(find(x)!=find(y)) return -1;
int ans=INF;
if(depth[x]<depth[y]) swap(x,y);
for(int i=20;i>=0;i--){
if(depth[fa[x][i]]>=depth[y]){
ans=min(ans,w[x][i]);
x=fa[x][i];
}
}
if(x==y) return ans;
for(int i=20;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
ans=min(ans,min(w[x][i],w[y][i]));
x=fa[x][i],y=fa[y][i];
}
}
return min(ans,min(w[x][0],w[y][0]));
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
int u,v,q;
for(int i=1;i<=m;i++){
scanf("%d %d %d",&u,&v,&q);
a[i].x=u;
a[i].y=v;
a[i].dis=q;
}
kruscal();
pre_LCA();
int Q;
scanf("%d",&Q);
while(Q--){
scanf("%d %d",&u,&v);
printf("%d\n",LCA(u,v));
}
return 0;
}
这...也算是紫题?
我们可以发现,树上三个点的三对 \(LCA\) 一定有两个是相同的,这是一件想想的话比较显然的事情。
必然能够找到某个节点,让三个点中的两个在一侧,一个在另一侧。而这个点就是两个公共的 \(LCA\)。
思考的再深入些(并且结合瞎蒙),我们会发现这个相同的 \(LCA\) 肯定是深度最小的一个 \(LCA\)。
这里,我们首先可以显而易见的发现,这个点必须在三个点互相通达的路径上。
我们再思考一下 \(LCA\) 与路径和的关系。
假设我们知道 \(a\) 和 \(b\) 的 \(LCA\) 是 \(x\),而且 \(x\) 是上述的 \(3\) 个 \(LCA\) 中深度最大的那个。
那么可以发现从 \(x\) 到 \(a\) 的距离加上从 \(x\) 到 \(b\) 的距离一定是最小的。
根据上面的结论,我们知道 \(a\),\(c\) 和 \(b\),\(c\) 的 \(LCA\) 点 \(y\) 一定在一个点上,而且这个 \(y\) 一定比 \(x\) 深度小。
那么这个时候,我们会发现此时 \(a\),\(b\),\(c\) 到 \(x\) 的距离和是最小的。证明的话可以这么想:
如果 \(x′\) 比 \(x\) 高,那么虽然 \(c\) 到 \(x\) 的距离减小了 \(w\),但是 \(a\),\(b\)到 \(x‘\) 的距离均增大了 \(w\),显然距离和增大。
如果 \(x′\) 比 \(x\) 低,有一个节点到 \(x′\) 的距离减小了 \(w\),剩下两个节点到 \(x′\) 的距离均增大了 \(w\),显然距离和也增大。
所以我们就找到了到三个点距离和最小的点:这三个点的三对 \(LCA\) 中,深度大的那两个 \(LCA\) 就是答案。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#define N 500010
using namespace std;
int n,m,head[N],cnt=0;
int d[N],f[N][25];
struct node{
int next,to;
}edge[N<<1];
int read(){
int x=0,f=1;char c=getchar();
while(c<‘0‘ || c>‘9‘) f=(c==‘-‘)?-1:1,c=getchar();
while(c>=‘0‘ && c<=‘9‘) x=x*10+c-48,c=getchar();
return x*f;
}
void addedge(int x,int y){
cnt++;
edge[cnt].next=head[x];
edge[cnt].to=y;
head[x]=cnt;
return;
}
void dfs(int x,int fa){
d[x]=d[fa]+1;
f[x][0]=fa;
for(int i=1;(1<<i)<=d[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].to!=fa) dfs(edge[i].to,x);
}
return;
}
int LCA(int x,int y){
if(d[x]>d[y]) swap(x,y);
for(int i=20;i>=0;i--)
if(d[f[y][i]]>=d[x]) y=f[y][i];
if(x==y) return x;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
int main(){
n=read();m=read();
int x,y,z;
for(int i=1;i<n;i++){
x=read();y=read();
addedge(x,y);addedge(y,x);
}
dfs(1,0);
for(int i=1;i<=m;i++){
x=read();y=read();z=read();
int p1=LCA(x,y);
int p2=LCA(y,z);
int p3=LCA(x,z);
int ans;
if(p1==p2) printf("%d ",p3);
else if(p2==p3) printf("%d ",p1);
else if(p1==p3) printf("%d ",p2);
printf("%d\n",d[x]+d[y]+d[z]-d[p1]-d[p2]-d[p3]);
}
return 0;
}
这...就结束了。
完结撒花。
貌似忘了什么重要的事情:
再次完结撒花。
标签:lca str 拆分 调整 点距 ahoi2008 last tps 基本
原文地址:https://www.cnblogs.com/lpf-666/p/12606460.html