树链剖分(含例题)

树链剖分介绍:

模板题:【模板】最近公共祖先(LCA) - 洛谷

 图片来自:通俗易懂的树链剖分详解 - 一剑缥缈的洛咕博客 - 洛谷博客

树链剖分的优点:

求LCA中的优化无非就是减少无效搜索的次数,树链剖分把树拆成不同的链(保证不重不漏),因为两个点的LCA一定在一条链上(同一个点),所以如果搜索到的点不在同一条链上,那么不是倍增跳或是······,而是跳过一整条链,这样搜索的速度会大幅提高。

注释代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 500010;

struct edge{
	int to,ne;
}edges[1000005];

int n,m,root;
int son[N],size[N],top[N],dep[N],f[N],idx,h[N];

void add(int u,int v)
{
	++ idx;
	edges[idx].to = v;
	edges[idx].ne = h[u];
	h[u] = idx;
	++ idx;
	edges[idx].to = u;
	edges[idx].ne = h[v];
	h[v] = idx;
}
//size树的大小,dep是点的深度,f是父节点,son是重儿子
void dfs1(int u){
	size[u] = 1;
	dep[u] = dep[f[u]] + 1;
	for(int i = h[u];i;i = edges[i].ne)
	{
		int v = edges[i].to;
		if(v == f[u])continue;
		f[v] = u;
		dfs1(v);
		size[u] += size[v];
		if(size[son[u]] < size[v])//求出重儿子
			son[u] = v;
	}
}

void dfs2(int u,int tp)//tp是链的编号(顶点) ,用于区分不同的链
{
	top[u] = tp;
	if(son[u])//是重链则一直顺下去,保持链的编号不变
		dfs2(son[u],tp);
	
	for(int i = h[u];i;i = edges[i].ne)
	{
		int v = edges[i].to;
		if(v != f[u] && v != son[u])
			dfs2(v,v);//重开一条链
	}
}

int lca(int u,int v)
{
	while(top[u] != top[v])//如果两个点不在同一条链上,直接跳过一整条链
	{
		
		if(dep[top[u]] < dep[top[v]])//保持相对大小不变
			swap(u,v);
		
		u = f[top[u]];
	}
	return dep[u] < dep[v] ? u : v;//处于同一条链上,谁跳到上端谁是公共节点
}

int main()
{	
	int x,y;
	
	cin >> n >> m >> root;
	for(int i = 1;i < n;i ++)
	{
		cin >> x >> y;
		add(x,y);
	}
	dfs1(root);
	dfs2(root,root);
	for(int i = 1;i <= m;i ++)
	{
		cin >> x >> y;
		cout << lca(x,y) << endl;
	}
}

T4烤乐滋野餐

题目介绍:

 

 解题思路:

这道题为什么跟LCA有关系呢?

首先看题目数据,n个点,n - 1条边,那么图大概是这个形状:

 题中给定两个点,求这两个点经过的距离和(默认要小),两个点的食物和。

显然,我们可以看到只是求图中关于这两个点的局部。(在上图中,画紫色圈的是x,y),那么我们肉眼可确立路径,5,2,6,11。

我们必然要找LCA作为“拐点”,同时利用树链剖分的性质结合容斥原理,可以快速计算出距离和以及食物和。

1.5,2,6,11的距离和可以看成蓝条 + 红条 - (1,2) - (1)。

对应代码:

ans1=val[x]+val[y]-val[LCA]-val[fa[LCA]]

val是如此得出的

void dfs1(int now,int fath,int diss,int var)//now是当前这个点,fath是父节点,diss(dis)是距离和,var是食物的和
{
	fa[now]=fath;dep[now]=dep[fath]+1;
	sz[now]=1;dis[now]=diss;
	val[now]=var;
	for(int i=head[now];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fath)
			continue;
		dfs1(v,now,diss+w[i],var+va[v]);
		sz[now]+=sz[v];
		if(sz[v]>sz[son[now]])
			son[now]=v;
	}
}

2.同理,距离和更好算

在上图中:5 - 2的距离即为dis[x] - dis[LCA],2 - 11 的距离即为dis[y] - dis[LCA]

对应代码:ans2=dis[x]+dis[y]-2*dis[LCA]

 可以发现,上述两个结论具有普遍意义。

 于是,我们用树链剖分 + 类似前缀和 得到了更加高效的计算方式。

完整代码

#include<bits/stdc++.h>

using namespace std;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f; 
}

const int N=200005;

int to[N],head[N],nxt[N],w[N],cnt;

void add(int u,int v,int wi)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	w[cnt]=wi;
	head[u]=cnt;
}

int dep[N],top[N],sz[N],fa[N],son[N];
int dis[N],val[N],va[N];//va表示食物的数量

/*
	dfs1,dfs2,lca树链剖分
*/
void dfs1(int now,int fath,int diss,int var)//now是当前这个点,fath是父节点,diss(dis)是距离和,var是食物的和
{
	fa[now]=fath;dep[now]=dep[fath]+1;
	sz[now]=1;dis[now]=diss;
	val[now]=var;
	for(int i=head[now];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fath)
			continue;
		dfs1(v,now,diss+w[i],var+va[v]);
		sz[now]+=sz[v];
		if(sz[v]>sz[son[now]])
			son[now]=v;
	}
}

void dfs2(int now,int t)
{
	top[now]=t;
	if(!son[now])
		return;
	dfs2(son[now],t);
	for(int i=head[now];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fa[now]||v==son[now])
			continue;
		dfs2(v,v);
	} 
}

int lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])
			swap(x,y);
		x=fa[top[x]];
	}
	return dep[x]>dep[y]?y:x;
}

int n,q;

int gcd(int a,int b)
{
	if(a==0)
		swap(a,b);
	return b==0?a:gcd(b,a%b);//辗转相除法,模板很多种
}

int main()
{
	n=read();
	for(int i=1;i<=n;i++)
		va[i]=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),wi=read();
		add(u,v,wi);add(v,u,wi);
	}
	dfs1(1,0,0,va[1]);
	dfs2(1,1);
	q=read();
	while(q--)
	{
		int x=read(),y=read();
		int LCA=lca(x,y);
		int ans1=val[x]+val[y]-val[LCA]-val[fa[LCA]],ans2=dis[x]+dis[y]-2*dis[LCA];//见博客
		int GCD=gcd(ans1,ans2);
		printf("%d/%d\n",ans1/GCD,ans2/GCD);
	}
	return 0;
}

posted @ 2022-09-24 21:16  zyc_xianyu  阅读(23)  评论(0编辑  收藏  举报