树
1. 树的直径
- 定义:树的任意两个节点之间最长的路径
-
一颗树可以有很多条直径
如图,任意黄色节点到红色节点都可以是直径
-
树的直径的中间节点被称为树的中心(图中m节点)。
如果一条直径上有偶数个点,那么中间两个点都可以称作树的中心。
树的中心到其他点到最长路径最短 -
求法:用两次搜索求得。第一次从任意一个节点开始搜索,找出最远节点a。第二次从a节点开始搜索,找到距离a节点最远的节点b。则路径a-b为树的直径
代码如下
# include<bits/stdc++.h>
# define int long long
using namespace std;
typedef pair<int,int> pii;
const int N = 5e5+10;
vector<pii> edge[N];
int dist[N];
void dfs(int x,int f)
{
for(auto t:edge[x])
{
if(t.first == f) continue;
dist[t.first] = dist[x]+t.second; dfs(t.first,x);
}
}
signed main()
{
int n;cin>>n;
for(int i=0;i<n-1;i++)
{
int u,v,w;cin>>u>>v>>w;
edge[u].push_back({v,w});
edge[v].push_back({u,w});
}
dfs(1,1);
int x,cnt = 0;
for(int i=1;i<=n;i++) //先进行一遍dfs后,遍历每个点离起始点的距离找出最远的点
{
if(dist[i]>cnt) {cnt = dist[i];x = i;}
}
memset(dist,0,sizeof dist); //将距离清0后再进行一遍dfs
dfs(x,x);
int ans = -1e9;
for(int i=1;i<=n;i++) ans = max(ans,dist[i]);
//找出第二次dfs遍历后离起始点最远的点就是树的直径
cout<<ans<<endl;
return 0;
}
2. 树的重心
对于一颗无根树,当一个节点被选为根节点,它底下每个子节点的子树的大小最大值最小。那么这个结点就是树的重心
如图p或q为树的重心
- 当重心为根节点时,它底下每个子树的大小不大于整棵树大小的一半
- 重心到其他所有节点的距离和最小
给你一棵 n个节点的树(节点的编号为 1到 n),现在我们想从树中选出一个节点,使得这个节点到其它所有节点的距离之和最小,请问距离和最小是多少?
例题 最小路径和 http://oj.daimayuan.top/course/7/problem/530
输入格式
第一行一个整数 n表示节点数。接下来 n−1行,每行两个整数 x,y表示 x号节点和 y号节点之间有一条边。
输入保证是一棵树。
输出格式
输出一行一个数,表示最小距离和。样例输入
4
1 2
1 3
3 4
样例输出
4
数据规模
对于所有数据,保证 1≤n≤100000,1≤x,y≤n。
# include<bits/stdc++.h>
# define int long long
using namespace std;
const int N = 1e5+10;
vector<int> edge[N];
int dist[N]; //当前节点所有子树的总和大小
int ans[N];
int n;
void solve(int x,int f)
{
for(auto t:edge[x])
{
if(t == f) continue;
solve(t,x);
dist[x]+=dist[t]; //可以自底向上同时求子树总和大小和每个节点的最大子树
ans[x] = max(ans[x],dist[t]);
}
ans[x] = max(ans[x],n-dist[x]);
}
void dfs(int x,int f)
{
for(auto t:edge[x])
{
if(t == f) continue;
dist[t] = dist[x]+1;
dfs(t,x);
}
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) dist[i] = 1;
for(int i=0;i<n-1;i++)
{
int x,y;cin>>x>>y;
edge[x].push_back(y);edge[y].push_back(x);
}
solve(1,1); //求出每个节点为根节点的时候最大子树的大小
int res = 0x3f3f3f3f;
int x;
for(int i=1;i<=n;i++)
{
if(ans[i]<res)
{
res = ans[i];x = i; //最大的子树最小的为树的重心,x最后为树的重心
}
}
memset(dist,0,sizeof dist);
dfs(x,x); //求出以树的重心为根节点的时候,各个节点到根节点的距离
res = 0;
for(int i=1;i<=n;i++) res+=dist[i]; //最后算出距离和
cout<<res<<endl;
return 0;
}
3. 树的最近公共祖先(lca)
lca:对于两个节点u,v,找到一个树深度最大的x,x是u,v的祖先。
多个节点的lca问题可以转化为多次求两个节点lca的形式
求解方法
- 预处理出数组fa[x][k]表示x跳2^k到达的点
- 将u,v调整到同一深度
- u,v同时往上跳,最后会跳到只差一步就到达最小公共祖先的位置
一些细节和关于正确性的证明写在了例题代码的注释里
例题
祖先询问 https://www.acwing.com/problem/content/1174/
# include<bits/stdc++.h>
using namespace std;
const int N = 4e4+10;
vector<int> edge[N];
int root;
int fa[N][20]; //fa[x][k]是x跳2^k步后落到的点
int dist[N]; //dist[x]为点x的深度。根节点深度为1,目的是为了让跳到根节点上方的点x的深度=0
void dfs(int x,int f) //递归算出各个节点的深度,且对于每个节点遍历一遍k
{
for(auto t:edge[x])
{
if(t == f) continue;
dist[t] = dist[x]+1;
fa[t][0] = x;
for(int k=1;k<=15;k++) fa[t][k] = fa[fa[t][k-1]] [k-1]; //将跳2^k步转化为跳两次2^(k-1)步
dfs(t,x);
}
}
int lca(int x,int y)
{
if(dist[x]<dist[y]) swap(x,y); //保证x总是在y的下方
for(int k=15;k>=0;k--)
{
if( dist[fa[x][k]]>=dist[y] ) x = fa[x][k]; //将x跳到与y同一深度
//k从大到小枚举,相当于枚举这个16位(根据数据范围假设)的数的第k+1位是否为1
//从高位往低位枚举才能保证正确,因为只要高位这位如果是0,那么就不会被误认为是1。因为10xxx一定是小于11xxx的。
//正确的步数如果是前者,那么后者必然会导致if语句的条件不成立
//反之如果k从低位往高位枚举则不能保证正确
}
if(x == y) return x;
for(int k=15;k>=0;k--)
{
if(fa[x][k]!=fa[y][k]) //如果跳2^k步是两点的公共祖先就不跳
{ //那么这样一直试探会发现最后的状态是两个点距离最小公共祖先都为1。所以最后返回fa[x][0]或者fa[y][0]
x = fa[x][k];y = fa[y][k];
}
}
return fa[x][0];
}
int main()
{
int n;cin>>n;
for(int i=0;i<n;i++)
{
int x,y;cin>>x>>y;
if(y == -1) {root = x;continue;}
edge[x].push_back(y);edge[y].push_back(x);
}
dist[root] = 1;
dfs(root,root);
int m;cin>>m;
while(m--)
{
int x,y;cin>>x>>y;
int p = lca(x,y);
if(p == x) cout<<"1\n";
else if(p == y) cout<<"2\n";
else cout<<"0\n";
}
return 0;
}