在学树链剖分前 先回顾两个算法
树上差分
树上差分,是一个适用于树上区间操作的算法 它是差分数组,前缀和求解的树上拓展。
其中 树上差分 99%的可能性与LCA一起出现在题目中
那么关于树上差分的问题来了
1.如何给树上的一条链(x~y)加上1(x为y的祖先)
还是要回到差分数组考虑 设原数组为a 差分数组为d
假设给d[i]+1 就相当于给a[i]~a[n]+1
那么如果给树上的一个节点x d[x]+1 就只能是x到根节点这条链+1 即d[x]+1 相当于a[root]~a[x]+1
所以区间修改就是d[fa[x]]-1,d[y]+1 (fa[x]为x的父亲) 即 a[y]~a[root]+1 a[fa[x]]~a[root]-1
2.如何给树上的任意一条链(x~y)加上1
我们可以找到x y的公共祖先lca(x,y) 将这条链拆开 变成x~lca(x,y) y~lca(x,y)两条链
但是这个时候发现 我们给lca(x,y)节点+2了 所以我们可以在第二次减的时候 只让lca(x,y)的父亲节点到根节点-1
所以最后 树上修改就变成了:
d[x]+1 d[y]+1 d[lca(x,y)]−1 d[fa[lca(x,y)]]−1
LCA
对于一棵树 求两个节点的最近公共祖先
如图 1和6的LCA是8 11和1的LCA是8 11和15的LCA是4 14和13的LCA是1
LCA有在线和离线两种算法
1.Tarjan:
Tarjan算法基于dfs 在dfs的过程中 对于每个节点的位置的询问做出相应的回答
dfs的过程中 当一棵子树被搜索完成之后 就把他和他的父亲节点合并成同一集合 在搜索当前子树节点的询问时 如果该询问的另一个节点已经被访问过 那么该编号的询问时被标记了的 于是输出当前状态下 另一个节点所在并查集的祖先 如果另一个节点还没被访问过 那么做下标记 继续dfs
比如8-1-14-13 此时8已经完成了对子树1的子树14的dfs与合并 如果查询时存在询问(13,14) 则其LCA即fa(14) 然后处理完由13和已经完成搜索的子树的询问 然后合并子树13的集合与fa(13)的集合 回溯到fa(13) 并dfs完所有1的其他未被搜索过的儿子 并完成子树1的所有节点的合并 再往fa(1)回溯
树上最短路:
const int maxn = 40005;
int father[maxn],ans[maxn],dis[maxn];
int vis[maxn];
int u,v;
int tot,head[maxn];
struct Edge
{
int dest;
int next;
int weight;
} edge[maxn*2];
vector<int> query[maxn],query_id[maxn];
void add(int u,int v,int l)
{
edge[++tot].dest=v;
edge[tot].next=head[u];
edge[tot].weight=l;
head[u]=tot;
}
int find(int x)
{
if(father[x]==x)
return x;
return father[x]=find(father[x]);
}
void Tarjan(int x)
{
vis[x]=1;
for(int i=head[x]; i!=0; i=edge[i].next)
{
int y=edge[i].dest;
if(vis[y])
continue;
dis[y]=dis[x]+edge[i].weight;
Tarjan(y);
father[y]=x;
}
for(int i=0; i<query[x].size(); i++)
{
int y=query[x][i],id=query_id[x][i];
if(vis[y]==2)
{
int lca=find(y);
ans[id]=min(ans[id],dis[x]+dis[y]-2*dis[lca]);
}
}
vis[x]=2;
}
int main()
{
int T,n,m;
scanf("%d",&T);
while(T--)
{
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
scanf("%d %d",&n,&m);
for(int i=1; i<=n; i++)
{
father[i]=i;
query[i].clear();
query_id[i].clear();
}
tot=0;
for(int i=1; i<n; i++)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
add(x,y,z),add(y,x,z);
}
for(int i=1; i<=m; i++)
{
int x,y;
scanf("%d %d",&x,&y);
if(x==y)
ans[i]=0;
else
{
query[x].push_back(y),query_id[x].push_back(i);
query[y].push_back(x),query_id[y].push_back(i);
ans[i]=1<<30;
}
}
Tarjan(1);
for(int i=1; i<=m; ++i)
printf("%d\n",ans[i]);
}
}
2.倍增
struct node
{
int t,nex;
} e[500001<<1];
int depht[500001],father[500001][22],lg[500001],head[500001];
int tot;
inline void add(int x,int y)
{
e[++tot].t=y;
e[tot].nex=head[x];
head[x]=tot;
}
inline void dfs(int now,int fath)
{
depht[now]=depht[fath]+1;
father[now][0]=fath;
for(register int i=1; (1<<i)<=depht[now]; ++i)
father[now][i]=father[father[now][i-1]][i-1];
for(register int i=head[now]; i; i=e[i].nex)
{
if(e[i].t!=fath)
dfs(e[i].t,now);
}
}
inline int lca(int x,int y)
{
if(depht[x]<depht[y])
swap(x,y);
while(depht[x]>depht[y])
x=father[x][lg[depht[x]-depht[y]]-1];
if(x==y)
return x;
for(register int k=lg[depht[x]]; k>=0; --k)
if(father[x][k]!=father[y][k])
x=father[x][k],y=father[y][k];
return father[x][0];
}
int n,m,s;
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(register int i=1; i<=n-1; ++i)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(s,0);
for(register int i=1; i<=n; ++i)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
for(register int i=1; i<=m; ++i)
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}
那么如果将x~y+1 求x~y最短路径上节点之和 变成一道题目的两种操作 那么显然 上面两种方法都不能用了
树链剖分的作用:
树剖是通过轻重边剖分将树分割成多条链 然后利用数据结构来维护这些链 本质是一种优化后的暴力
基础概念:
重儿子:父亲节点的所有儿子中子树结点数目最多的
轻儿子:父亲节点 中除了重儿子以外的儿子
重边:父亲节点和重儿子连成的边
轻边:父亲节点和轻儿子连成的边
重链:由多条重边连接成的路径
轻链:由多条轻边连接成的路径
f[u]:保存节点u的父亲节点
d[u]:保存节点u的深度值
size[u]:保存以u为根的子树节点个数
son[u]:保存重儿子
rank[u]:保存当前dfs标号在树中对应的节点
top[u]:保存当前节点所在链的顶端节点
id[u]:保存树中每个节点剖分以后的新编号(DFS的执行顺序)
树链剖分的实现:
1.对于一个点我们首先求出它所在子树的大小,找出他的重儿子(即处理处size son数组)
例子:
对于点1 它有三个儿子 2,3,4
2 所在的子树大小是5;3 所在的子树大小是2;4 所在子树的大小是6 那么1的重儿子是4
如果一个点的多个儿子所在子树大小相等且最大 那随便找一个当做它的重儿子
叶子节点没有重儿子 非叶节点有且只有一个重儿子
2.在dfs过程中顺便记录其父亲以及深度(即处理出f,d数组) 操作1,2可以通过一遍dfs完成
void dfs1(int u,int f,int deep) //当前节点、父节点、层次深度
{
f[u]=fa,d[u]=deep,size[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].dest;
if(v==fa) continue;
dfs1(v,u,deep+1);
size[u]+=size[v];
if(size[v]>size[son[u]]) son[u]=v;
}
}
3.第二遍dfs,连接重链,同时标记每一个节点的dfs序,并且为了用数据结构维护重链,我们在dfs时保证一条重链上各个节点dfs序连接(即处理出top,id,rank数组)
void dfs2(int u,int t) //当前节点、重链顶端
{
top[u]=t;
id[u]=++cnt; //标记dfs序
rank[cnt]=u; //序号cnt对应节点u
if(!son[u]) return;
dfs2(son[u],t); //我们选择优先进入重儿子来保证一条重链上各店dfs连续
//一个点和它的重儿子处于同一条重链 所以重儿子所在重链
//的顶端还是t
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].dest;
if(v!=son[u]&&v!=f[u])
dfs2(v,v); //一个点位于轻链底端,那么它的top必然是它本身
}
}
4.两遍dfs就是树链剖分的主要处理 通过dfs我们已经保证一条重链上各个节点dfs序连续,那么可以想到,我们可以通过数据结构来维护一条重链的信息
比如之前的那个题目 修改和查询的操作原理是类似的 以查询操作为例 其实就是个LCA 不过这里是用了top来进行加速 因为top可以直接跳转到该重链的起始节点 轻链没有起始节点之说,他们的top就是自己。需要注意的是,每次循环只能跳一次,并且让节点深的那个来跳到top的位置
int sum(int x,int y)
{
int ans=0,fx=top[x],fy=top[y];
while(fx!=fy) //如果两点不在同一条重链
{
if(d[fx]>=d[fy])
{
ans+=query(id[fx],id[x],rt); //线段树区间求和,处理这条重链的贡献
x=f[fx],fx=top[x]; //将x设置成原链头的父亲节点,走轻边,继续循环
}
else
{
ans+=query(id[fy],id[y],rt);
y=f[fy],fy=top[y];
}
}
//循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点的贡献
if(id[x]<=id[y])
ans+=query(id[x],id[y],rt);
else
ans+=query(id[y],id[x],rt);
return ans;
}