浅谈LCA
LCA 之前在倍增的时候是提到过的。求 LCA 的办法有倍增求 LCA 以及树剖求 LCA。对于离线的求法还有 Tarjan。但是由于需要并查集,而且树剖的常数有很小,所以一般我们不用离线方法。忘了说了,在线求法还有两遍 access LCT 哦。
倍增法常数较大,树剖法常数极小。对于树上修改与查询,一般用树剖。
LCA 还有一个用处是去笛卡尔树上求 RMQ。这个以后会提到。
没了吧,LCA 的用法就这么写,看题。
P3379 【模板】最近公共祖先(LCA)
需要讲吗?
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
vector<int> G[N];
int n,m,s,dep[N],f[N][25];
void dfs(int x,int fa)
{
f[x][0]=fa,dep[x]=dep[fa]+1;
for(int i=1;(1<<i)<=n;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int v:G[x])
{
if(v==f[x][0]) continue;
dfs(v,x);
}
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;~i;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=20;~i;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<n;i++)
{
int x,y;cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
dfs(s,0);
while(m--)
{
int x,y;cin>>x>>y;
cout<<LCA(x,y)<<"\n";
}
return 0;
}
P3128 [USACO15DEC] Max Flow P
这道题就是树上的区间问题。为了不炸管道,我们必须保证流量不超过 \(s\) 到 \(t\) 上的最小值。直接树剖即可。当然也可以倍增,鉴于上一道题写的倍增,这道题我们采用树剖法。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
vector<int> G[N];
int n,m,TIME,hson[N],dep[N],sz[N],dfn[N],tp[N],f[N];
void dfs1(int x,int fa)
{
sz[x]=1,hson[x]=-1;
dep[x]=dep[fa]+1;
for(int v:G[x])
{
if(v==fa) continue;
dfs1(v,x);
f[v]=x;
sz[x]+=sz[v];
if(hson[x]==-1||sz[hson[x]]<sz[v])
hson[x]=v;
}
}
void dfs2(int x,int t)
{
dfn[x]=++TIME;
tp[x]=t;
if(hson[x]==-1) return;
dfs2(hson[x],t);
for(int v:G[x])
if(v!=hson[x]&&v!=f[x]) dfs2(v,v);
}
template<typename TT> class SGT
{
int T[N<<2],tag[N<<2];
void pushup(int x)
{
T[x]=max(T[x<<1],T[x<<1|1]);
}
void pushdown(int x)
{
if(!tag[x]) return;
tag[x<<1]+=tag[x];
tag[x<<1|1]+=tag[x];
T[x<<1]+=tag[x];
T[x<<1|1]+=tag[x];
tag[x]=0;
}
public:
void upd(int x,int l,int r,int ql,int qr,int k)
{
if(ql<=l&&r<=qr)
{
T[x]+=k,tag[x]+=k;
return ;
}
int mid=(l+r)>>1;
pushdown(x);
if(ql<=mid) upd(x<<1,l,mid,ql,qr,k);
if(qr>mid) upd(x<<1|1,mid+1,r,ql,qr,k);
pushup(x);
}
int query(int x,int l,int r,int ql,int qr)
{
if(ql<=l&&r<=qr) return T[x];
int mid=(l+r)>>1,res=0;
pushdown(x);
if(ql<=mid) res=max(res,query(x<<1,l,mid,ql,qr));
if(qr>mid) res=max(res,query(x<<1|1,mid+1,r,ql,qr));
return res;
}
};
SGT<int> T;
void upd_P(int x,int y)
{
while(tp[x]!=tp[y])
{
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
T.upd(1,1,n,dfn[tp[x]],dfn[x],1);
x=f[tp[x]];
}
if(dep[x]<dep[y]) swap(x,y);
T.upd(1,1,n,dfn[y],dfn[x],1);
}
int main()
{
cin>>n>>m;
for(int i=1;i<n;i++)
{
int x,y;cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
dfs1(1,0);
// for(int i=1;i<=n;i++)
// cout<<hson[i]<<" ";
// cout<<"\n";
dfs2(1,1);
// cout<<1<<endl;
while(m--)
{
int x,y;cin>>x>>y;
upd_P(x,y);
}
cout<<T.query(1,1,n,dfn[1],dfn[1]+sz[1]-1)<<"\n";
return 0;
}
P3258 [JLOI2014] 松鼠的新家
稍微转换一下题面,就是进行 \(n-1\) 轮操作,每一次让 \(p_i\) 和 \(p_{i+1}\) 的树上最短路径上的点权加一。可以树剖,当然可以用差分做,这样可以减少码长。
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n,p[N],f[N][25],dep[N],pre[N];
vector<int> G[N];
void dfs(int x,int y)
{
f[x][0]=y,dep[x]=dep[y]+1;
for(int i=1;i<=20;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int v:G[x])
{
if(v==y) continue;
dfs(v,x);
}
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;~i;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=20;~i;i--)
{
if(f[x][i]==f[y][i]) continue;
x=f[x][i],y=f[y][i];
}
return f[x][0];
}
void dfs2(int x,int y)
{
for(int v:G[x])
{
if(v==y) continue;
dfs2(v,x);
pre[x]+=pre[v];
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>p[i];
for(int i=1;i<n;i++)
{
int u,v;cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
for(int i=1;i<n;i++)
{
int u=p[i],v=p[i+1];
pre[u]++,pre[v]++;
int L=LCA(u,v);
pre[L]--,pre[f[L][0]]--;
}
dfs2(1,0);
for(int i=2;i<=n;i++)
pre[p[i]]--;
for(int i=1;i<=n;i++)
cout<<pre[i]<<"\n";
return 0;
}
P3938 斐波那契
规律比较明显。发现一个结点的父亲节点是他的节点编号减去他出生的月份。对于月份,我们直接二分即可。而这一颗树的高度有结论---不超过 \(\mathcal O(\log n)\),所以总复杂度就是 \(\mathcal O(n\log ^2n)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+10,M=65;
int f[N],n;
signed main()
{
f[1]=f[2]=1;
for(int i=3;i<=60;i++)
f[i]=f[i-1]+f[i-2];
cin>>n;
while(n--)
{
int x,y;cin>>x>>y;
while(x!=y)
{
if(x<y) swap(x,y);
int pos=lower_bound(f+1,f+61,x)-f;
x-=f[pos-1];
}
cout<<x<<"\n";
}
return 0;
}
P6869 [COCI 2019/2020 #5] Putovanje
这道题其实就是松鼠的新家那道题。我们发现这道题就是要计算每一个节点经过的次数。最后判断是单程好还是无限程好即可。可以差分也可以树剖。这里采用差分。
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+5;
int n,m,c1[N],c2[N],dep[N],f[N][25],pre[N],con[N];
vector<pii> G[N];
void dfs(int x,int fa)
{
f[x][0]=fa,dep[x]=dep[fa]+1;
for(int i=1;i<=18;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(auto [v,id]:G[x])
{
if(v==fa) continue;
con[v]=id;
dfs(v,x);
}
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=18;~i;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=18;~i;i--)
{
if(f[x][i]==f[y][i]) continue;
x=f[x][i],y=f[y][i];
}
return f[x][0];
}
void DFS(int x,int fa,int I)
{
for(auto [v,id]:G[x])
{
if(v==fa) continue;
DFS(v,x,id);
pre[I]+=pre[id];
}
}
signed main()
{
cin>>n;
for(int i=1;i<n;i++)
{
int u,v;cin>>u>>v>>c1[i]>>c2[i];
G[u].emplace_back(v,i);
G[v].emplace_back(u,i);
}
dfs(1,0);
for(int i=2;i<=n;i++)
{
int L=LCA(i-1,i);
pre[con[i-1]]++,pre[con[i]]++,pre[con[L]]-=2;
}
DFS(1,0,0);
int ans=0;
for(int i=1;i<n;i++)
ans+=min(pre[i]*c1[i],c2[i]);
cout<<ans;
return 0;
}
P4281 [AHOI2008] 紧急集合 / 聚会
很容发现一个结论:假设三个人分别在 \(x,y,z\),那么结合地点一定在 \(LCA(x,y),LCA(y,z),LCA(x,z)\) 其中一个。
然后做三遍 LCA 就行了。
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=5e5+5;
int n,m,dep[N],f[N][25];
vector<int> G[N];
void dfs(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];
for(int v:G[x])
{
if(v==fa) continue;
dfs(v,x);
}
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;~i;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=20;~i;i--)
{
if(f[x][i]==f[y][i]) continue;
x=f[x][i],y=f[y][i];
}
return f[x][0];
}
int dis(int x,int y)
{
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
int solve(int x,int y,int z,int p)
{
return dis(x,p)+dis(y,p)+dis(z,p);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<n;i++)
{
int u,v;cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
while(m--)
{
int x,y,z;cin>>x>>y>>z;
int mn=1e9,id=0,cnt,p;
p=LCA(x,y),cnt=solve(x,y,z,p);
if(mn>cnt) mn=cnt,id=p;
p=LCA(y,z),cnt=solve(x,y,z,p);
if(mn>cnt) mn=cnt,id=p;
p=LCA(x,z),cnt=solve(x,y,z,p);
if(mn>cnt) mn=cnt,id=p;
cout<<id<<" "<<mn<<"\n";
}
return 0;
}
P8201 [传智杯 #4 决赛] [yLOI2021] 生活在树上(hard version)
我们发现,这道题要满足 \(dis_{t,a}\oplus dis_{t,b}=k\iff w(a,b)\oplus k=p\),其中 \(w(a,b)\) 是 \(a\) 到 \(b\) 的树上路径的点权异或和,\(p\) 是 \(a\) 到 \(b\) 的书上路径上的点权。
说大白话,就是要求满足 \(a\) 到 \(b\) 的点权异或和再异或上 \(k\) 后的值,要出现在 \(a\) 到 \(b\) 的路径上的点权集合中。
这个很容易了吧,要么树上莫队,要么树上开主席树,咋做都可以。这里采用树剖后在链上二分查找的做法。复杂度虽然是 \(n\log^2 n\) 的,但肯定比主席树好写。
#include <bits/stdc++.h>
#define int long long
#define pb push_back
#define ep emplace_back
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
const int N = 1e6 + 5;
int dep[N], dfn[N], T, n, m, a[N], dis[N], fa[N], tp[N], hs[N], sz[N], cnt;
vector<int> G[N];
map<int, int> idx;
multiset<int> tmp[N];
void dfs1(int x, int f)
{
sz[x] = 1, dis[x] = dis[f] ^ a[x], dep[x] = dep[f] + 1, fa[x] = f;
hs[x] = 0;
for (int v : G[x])
{
if (v == f)
continue;
dfs1(v, x);
sz[x] += sz[v];
if (hs[x] == 0 || sz[hs[x]] < sz[v])
hs[x] = v;
}
}
void dfs2(int x, int t)
{
dfn[x] = ++T, tp[x] = t;
if (hs[x])
dfs2(hs[x], t);
for (int v : G[x])
{
if (v != hs[x] && v != fa[x])
dfs2(v, v);
}
}
int LCA(int x, int y)
{
while (tp[x] != tp[y])
{
if (dep[tp[x]] < dep[tp[y]])
swap(x, y);
x = fa[tp[x]];
}
return dep[x] < dep[y] ? x : y;
}
bool check(int x, int y, int val_id)
{
while (tp[x] != tp[y])
{
if (dep[tp[x]] < dep[tp[y]])
swap(x, y);
auto pos1 = tmp[val_id].lower_bound(dfn[tp[x]]);
auto pos2 = tmp[val_id].upper_bound(dfn[x]);
if (pos1 != pos2)
return true;
x = fa[tp[x]];
}
if (dep[x] < dep[y])
swap(x, y);
auto pos1 = tmp[val_id].lower_bound(dfn[y]);
auto pos2 = tmp[val_id].upper_bound(dfn[x]);
return pos1 != pos2;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
if (!idx.count(a[i]))
idx[a[i]] = ++cnt;
}
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
G[u].pb(v), G[v].pb(u);
}
dfs1(1, 0);
dfs2(1, 1);
for (int i = 1; i <= n; i++)
{
tmp[idx[a[i]]].insert(dfn[i]);
}
while (m--)
{
int A, B, K;
cin >> A >> B >> K;
int L = LCA(A, B);
int target = dis[A] ^ dis[B] ^ a[L] ^ K;
if (!idx.count(target))
{
cout << "No\n";
continue;
}
int val_id = idx[target];
if (check(A, B, val_id))
cout << "Yes\n";
else
cout << "No\n";
}
return 0;
}
P2934 [USACO09JAN] Safe Travel G
好题。这道题首先看到最短路,又看到删边,首先想到最短路树。
建出最短路树后,我们考虑题目要求的问题的做法。
我们考虑删掉一条边后加边。假设 \(d_u\) 表示 \(u\) 到 \(1\) 的距离,\(w_{u,v}\) 表示 \(u\) 和 \(v\) 的边权,并且保证 \((u,v)\) 没出现在最短路树上,我们首先写出式子:\(ans_i=\min_{u,v} (d_u+d_v+w_{u,v}-d_i)\)。其中对于相同的 \(i\),\(d_i\) 也是相同的,所以只需要找 \(d_u+d_v+w_{u,v}\) 的最小值即可。
我们对于未被加入最短路树的边排序,按照 \(d_u+d_v+w_{u,v}\) 的大小从小到大排。
那么问题来了,怎么来判断对于一组 \((u,v)\) 他能更新的 \(i\) 有哪些呢?我们发现,对于 \((u,v)\),所有的 \(i\) 是 \(u\) 或 \(v\) 的祖先且是 \(LCA(u,v)\) 的后代。那么这个样子这道题就做完了。我们只需要把做过去的 \(i\) 放到一个并查集里,表示做过了即可。复杂度为 \(\mathcal O(n\log n+m\log m)\)。
#include<bits/stdc++.h>
#define ll long long
#define pii pair<int,int>
#define pil pair<int,ll>
#define pli pair<ll,int>
#define fi first
#define se second
#define pb push_back
#define ep emplace_back
using namespace std;
const int N=4e5+5;
int f[N],n,m,vis[N],F[N],cnt,dep[N];
vector<pli> G[N];
priority_queue<pil> q;
ll dis[N],ans[N];
struct node
{
int u,v;ll w;
friend bool operator<(const node &A,const node &B)
{
return dis[A.u]+dis[A.v]+A.w<dis[B.u]+dis[B.v]+B.w;
}
}b[N];
void dij()
{
memset(dis,0x3f,sizeof(dis));
dis[1]=0,q.push({0,1});
while(!q.empty())
{
auto [_,x]=q.top();q.pop();
if(vis[x]) continue;
vis[x]=1;
for(auto [v,w]:G[x])
if(dis[v]>dis[x]+w)
{
dis[v]=dis[x]+w;
f[v]=x,dep[v]=dep[x]+1;
q.push({-dis[v],v});
}
}
}
int find(int x){return x==F[x]?x:F[x]=find(F[x]);}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v;ll w;scanf("%d%d%lld",&u,&v,&w);
G[u].ep(v,w),G[v].ep(u,w);
}
dij();
for(int i=1;i<=n;i++)
{
ans[i]=-1,F[i]=i;
for(auto [j,w]:G[i])
{
if(j==f[i]||i==f[j]||i>j) continue;
b[++cnt].u=i,b[cnt].v=j,b[cnt].w=w;
}
}
sort(b+1,b+1+cnt);
for(int i=1;i<=cnt;i++)
{
int u=b[i].u,v=b[i].v;ll w=b[i].w;
int fu=find(u),fv=find(v);
while(fu!=fv)
{
if(dep[fu]<dep[fv]) swap(fu,fv);
ans[fu]=dis[u]+dis[v]+w-dis[fu];
F[fu]=f[fu],fu=find(fu);
}
}
for(int i=2;i<=n;i++) printf("%lld\n",ans[i]);
return 0;
}

浙公网安备 33010602011771号