浅谈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;
}
posted @ 2025-05-02 18:06  I_AK_CTSC  阅读(25)  评论(0)    收藏  举报