树
一、树的重心
概念和性质
(1).概念
树的重心也叫树的质心。对于一棵树\(n\)个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。换句话说,删除这个点后最大连通块(一定是树)的结点数最小。
(2).性质
1.树中所有点到某个点的距离和中,到重心的距离和是最小的(实际应用中经常用到此性质)。
2.把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。
3.一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。
4.一棵树最多有两个重心,且相邻。
关于如何求树的重心?
求树的重心运用动态规划的思想,也就是树上跑DP。
先任选一个结点作为根节点,把无根树变成有根树,然后去\(dfs\),在 \(dfs\) 中计算每个子树的大小,记录「向下」的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到「向上」的子树的大小,然后就可以依据定义找到重心了。
Tips
树的重点可以说是树的平衡点,其使以它为根的树中所有的子树的节点数相近。
代码
//树的重点可以说是树的平衡点,其使以它为根的树中所有的子树的节点数相近
int d[N];
int ans;//ans是树的重心;
int maxn = N;
void dfs(int u,int fa)
{
d[u] = 1;
int mx = 0;
for(auto v:e[u])
{
if(v==fa)continue;
dfs(v,u);
d[u] += d[v];
mx = max(mx,d[v]);
}
mx = max(mx,n-d[u]);
if(mx<maxn)
maxn = mx,ans = u;
//若选取编号最小的节点,可更改为
/* if(mx<maxn||(mx==maxn&&ans>u))
maxn = mx,ans = u;
*/
}
板子题:P1395 会议
法一:
距离和最小,恰好符合我们重心的性质。重心就是我们要求的点,在以重心为源点bfs求距离和即可。
求所有点到某个点的距离和最小,求出那个点和距离和。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e4+10,M = 2e6+10;
int idx,n,road;
vector<int> e[N];
int dist[N];
bool vis[N];
int d[N];
int ans;//ans是树的重心;
int maxn = N;
void dfs(int u,int fa)
{
d[u] = 1;
int mx = 0;
for(auto v:e[u])
{
if(v==fa)continue;
dfs(v,u);
d[u] += d[v];
mx = max(mx,d[v]);
}
mx = max(mx,n-d[u]);
// if(mx<maxn)
// maxn = mx,ans = u;
//若选取编号最小的节点,可更改为
if(mx<maxn||(mx==maxn&&ans>u))
maxn = mx,ans = u;
}
void bfs(int s)
{
queue<int>q;
q.push(s);
while(!q.empty())
{
int x = q.front();
q.pop();
for(auto y:e[x])
{
if(vis[y]||y==s)continue;
vis[y] = true;
dist[y] = dist[x]+1;
road += dist[y];
q.push(y);
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i = 1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1,0);
bfs(ans);
cout<<ans<<" "<<road<<endl;
return 0;
}
法二:
或者也可以用换根dp来做,因为是无根树,我们考虑随便一个点当作根,此时的路径和是多少。然后考虑另一个点做根的情况。当然不用每次都求一次的。
如图考虑:
如果把根从fa换到它的儿子u会发生什么事情?
对于非u的子树部分到u的距离比到fa的距离多出了:\(1*(n-sz[u])\)整个这一部分(圈起来的)都要加上\(fa->u\)这条边。
对于u的子树部分,那么相反的,会少了:\(1*sz[u]\)。那么\(f[u] = f[fa]+n-2*sz[u]\)
考虑清楚这个,接下来我们只需要先预处理出\(sz\),再去求\(f\)数组就可以了。
由于\(f\)数组的递推关系,我们考虑先求出一个,因为是无根树,不妨求\(1\)为根的情况,这时候的\(f[1]\)等于其他点的深度和(注意-n,距离等于深度-1)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e4+10,M = 2e6+10;
int n;
vector<int> e[N];
int dist[N];
bool vis[N];
int d[N],sz[N],f[N];
//深度,子树大小,距离和
void dfs1(int u)
{
sz[u] = 1;
for(auto v:e[u])
{
if(d[v])continue;
d[v] = d[u] + 1;
dfs1(v);
sz[u] += sz[v];
}
}
void dfs2(int u,int fa)
{
f[u] = f[fa]+n-2*sz[u];
for(auto v:e[u])
{
if(v==fa)continue;
dfs2(v,u);
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i = 1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
d[1] = 1;
dfs1(1);
int maxn = 0,idx = 1;
for(int i = 1;i<=n;i++)maxn += d[i];
maxn -= n;
f[1] = maxn;
for(auto y:e[1])
dfs2(y,1);
for(int i = 2;i<=n;i++)
{
if(f[i]<maxn)maxn = f[i],idx = i;
}
cout<<idx<<" "<<maxn<<"\n";
return 0;
}
二、树的直径
什么是树的直径?
树上任意两节点之间最长的简单路径即为树的「直径」
求法:2次dfs。第一次从随便一个点开始搜,搜到最远的点,然后从这个最远的点再搜一次。得到的链就是树的直径。
//树的直径
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6;
std::vector<int>edges[N+1];
int n,l,pre[N+1],c[N+1],dist[N+1];
inline void dfs(int x)
{
for(auto y:edges[x])
{
if(y!=pre[x])
{
pre[y] = x;
dist[y]= dist[x]+1;
dfs(y);
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i = 1;i<n;i++)
{
int x,y;
cin>>x>>y;
edges[x].push_back(y);
edges[y].push_back(x);
}
memset(dist,0,sizeof(dist));
memset(pre,0,sizeof(pre));
pre[1] = -1;
dfs(1);
int idx = 0,v =0;
for(int i =1 ;i<=n;i++)
{
if(dist[i]>v)
{
v = dist[i],idx = i;
}
}
memset(dist,0,sizeof(dist));
memset(pre,0,sizeof(pre));
pre[idx] = -1;
dfs(idx);
v =0;
for(int i =1 ;i<=n;i++)
{
if(dist[i]>v)
{
v = dist[i];
}
}
cout<<v<<"\n";
return 0;
}
例题1:P5536 【XR-3】核心城市
题意:这 \(k\)座城市可以通过道路,在不经过其他城市的情况下两两相互到达。
定义某个非核心城市与这k座核心城市的距离为,这座城市与k座核心城市的距离的最小值。那么所有非核心城市中,与核心城市的距离最大的城市,其与核心城市的距离最小。你需要求出这个最小值。
思路:我们知道,直径是树里面最长的路径。我们要找到\(k\)个点,这\(k\)个点比如包括直径的中点(由于直径的定义其他点到中点的距离一定不大于直径两端到中点的距离(因为如果大于,就一定能找到一条比直径更长的路径)。我们以直径中点为根去\(dfs\),其他节点按照以自己为根的子树里面子树最大的深度-这个点的深度(\(maxdep_i-dep_i\))去从大到小排序(先把大的取了,消除掉)。连通性因为树边权为1,然后我们要选离中点远的点,那么离中点更近的肯定以及被选了(因为它更大),那么选的话是连续的,那么连通性就有保证了。那么答案就是第\(k+1\)个(第一个不能被选的是答案,因为sort过了)。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6;
std::vector<int>edges[N+1];
int n,l,k,pre[N+1],c[N+1],dist[N+1];
inline void dfs(int x)
{
for(auto y:edges[x])
{
if(y!=pre[x])
{
pre[y] = x;
dist[y]= dist[x]+1;
dfs(y);
}
}
}
int pos[N];
int dep[N],max_dep[N],ans[N];
void dfs2(int u,int fa)
{
//if(edges[u].size()==0)max_dep[u] = dep[u];
max_dep[u] = dep[u];
for(auto v:edges[u])
{
if(v==fa)continue;
dep[v] = dep[u] + 1;
dfs2(v,u);
max_dep[u] = max(max_dep[u],max_dep[v]);
}
}
bool cmp(int x,int y)
{
return x>y;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
cin>>n>>k;
for(int i = 1;i<n;i++)
{
int x,y;
cin>>x>>y;
edges[x].push_back(y);
edges[y].push_back(x);
}
memset(dist,0,sizeof(dist));
memset(pre,0,sizeof(pre));
pre[1] = -1;
dfs(1);
int idx = 0,v =0,p = 0;
for(int i =1 ;i<=n;i++)
{
if(dist[i]>v)
{
v = dist[i],idx = i;
}
}
memset(dist,0,sizeof(dist));
memset(pre,0,sizeof(pre));
pre[idx] = -1;
dfs(idx);
v = 0;
for(int i =1;i<=n;i++)
{
if(dist[i]>v)
{
p = i;
v = dist[i];
}
}
int cnt = 0;
while(p !=-1)
{
pos[++cnt] = p;
p = pre[p];
}
int mid_pos = pos[(cnt+1)/2];
dep[mid_pos] = 1;
dfs2(mid_pos,0);
for(int i = 1;i<=n;i++)
{
ans[i] = max_dep[i]-dep[i];
}
sort(ans+1,ans+1+n,cmp);
cout<<ans[k+1]+1<<"\n";
return 0;
}
三、树上差分
树上差分,什么意思嘞?就是树上做差分(feihua)。它有两种常见类型:1.边差分 2.点差分。
对于边差分裸题:给你一棵树,n次操作,每次把\(u..v\)路径上的权值加\(x\),最后问你某两点的路径和。
对于\(u..v\)路径上的权值加\(x\)怎么实现呢?这里我们就用到的是差分。
\(dlt[u]+=x,dlt[v]+=x,dlt[lac]-=2x\),这样加的作用只限制于\(u..v\),而对于\(lca\)的父亲是没有影响的。
对于点差分呢?和我们的边差分有所不同。
点差分裸题:有n次修改操作,每次把u..v的所有点权都加x,最后问点权最大的为多少。
我们与边差分不同的是,在\(lca\)处不是是\(dlt[lca]-=2x\),而是\(dlt[lca]-=x\),然后\(dlt[fa[lca]]-=x\)。这是因为点权的话,\(lca\)这个点也是包含在里面的,需要被算一次。
\(u\)从左边过来给\(lca\)算了一次,然后\(v\)从右边过来又给\(lca\)算了一次,但我只需要一次,那么\(dlt[lca]-=x\),而又不能蔓延到父亲,那\(fa[lca]-=x\)。
点差分模板题:[P3128 USACO15DEC] Max Flow P
四、树上LCA
- 树上LCA板子
#include<bits/stdc++.h>
using namespace std;
const int N = 5000010;
const int LOGN = 20;
int n, m, root, dep[N], fa[N][LOGN + 2];
vector<int> e[N];
void dfs(int u, int from)
{
dep[u] += dep[from] + 1;
for(auto v : e[u]) {
if(v == from) continue;
fa[v][0] = u;
dfs(v, u);
}
}
void lca_init()
{
for(int j = 1; j <= LOGN; j++)
for(int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
int lca_query(int u, int v)
{
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int j = LOGN; j >= 0; j--)
if(d & (1 << j))
u = fa[u][j];
if(u == v) return u;
for(int j = LOGN; j >= 0; j--)
if(fa[u][j] != fa[v][j])
u = fa[u][j],v = fa[v][j];
return fa[u][0];
}
int main()
{
cin>>n>>m>>root;
for(int i = 2; i <= n; i++)
{
int u, v; cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(root, 0);
lca_init();
while(m--)
{
int u, v; cin>>u>>v;
cout<<lca_query(u, v)<<endl;
}
return 0;
}
几个经典例题:
例题1:P5836 Milk Visits S
做法1:
这个题的前置知识点:LCA,树上路径最值
树上路径最小值板子:路径最小值
#include<bits/stdc++.h>
using namespace std;
const int N = 201000;
const int LOGN = 18;
vector<pair<int,int>>edge[N];
int n,q;
int val[N][LOGN+1],f[N][LOGN+1],dep[N];
void dfs(int u,int fa)
{
dep[u] = dep[fa]+1;
for(auto i:edge[u])
{
int v = i.first;
if(v==fa)continue;
f[v][0] = u;
val[v][0] = i.second;
dfs(v,u);
}
}
int query(int u,int v)
{
int ans = 1<<30;
if(dep[u]>dep[v])
swap(u,v);
int d = dep[v]-dep[u];
for(int j = LOGN;j>=0;j--)
{
if(d&(1<<j))
{
ans = min(ans,val[v][j]);
v = f[v][j];
}
}
if(u==v)return ans;
for(int j = LOGN;j>=0;j--)
{
if(f[u][j]!=f[v][j])
{
ans = min({ans,val[u][j],val[v][j]});
u = f[u][j],v = f[v][j];
}
}
ans = min({ans,val[u][0],val[v][0]});
return ans;
}
int main()
{
cin>>n>>q;
for(int i = 1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
edge[u].push_back({v,w});
edge[v].push_back({u,w});
}
dfs(1,0);
for(int j = 1;j<=LOGN;j++)
{
for(int u = 1;u<=n;u++)
{
f[u][j] = f[f[u][j-1]][j-1];
val[u][j] = min(val[u][j-1],val[f[u][j-1]][j-1]);
}
}
for(int i = 1;i<=q;i++)
{
int u,v;
cin>>u>>v;
cout<<query(u,v)<<endl;
}
return 0;
}
对于本题:因为只有两种牛,我们对第一种牛标记为1,第二种标记为2。我们考虑把点的权值转化为边的权值。给出边的关系的时候,如果两个颜色一样那就标记这个颜色,否则标记为1+2 = 3。查询的时候如果是3,一定是yes,否则看是不是需要的就行。注意:如果给的是单点注意特判。
#include<bits/stdc++.h>
using namespace std;
const int N = 201000;
const int LOGN = 18;
vector<pair<int,int>>edge[N];
int n,m;
int val[N][LOGN+1],f[N][LOGN+1],dep[N];
int a[N];
void dfs(int u,int fa)
{
dep[u] = dep[fa]+1;
for(auto [v,w]:edge[u])
{
if(v==fa)continue;
f[v][0] = u;
val[v][0] = w;
dfs(v,u);
}
}
int query(int u,int v)
{
int ans = 0;
if(dep[u]>dep[v])
swap(u,v);
int d = dep[v]-dep[u];
for(int j = LOGN;j>=0;j--)
{
if(d&(1<<j))
{
ans = max(ans,val[v][j]);
v = f[v][j];
}
}
if(u==v)return ans;
for(int j = LOGN;j>=0;j--)
{
if(f[u][j]!=f[v][j])
{
ans = max({ans,val[u][j],val[v][j]});
u = f[u][j],v = f[v][j];
}
}
ans = max({ans,val[u][0],val[v][0]});
return ans;
}
int main()
{
cin>>n>>m;
for(int i = 1;i<=n;i++)
{
char x;
cin>>x;
if(x=='H')
a[i] = 1;
else a[i] = 2;
}
for(int i = 1;i<n;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back({v,(a[u]==a[v]?a[u]:a[u]+a[v])});
edge[v].push_back({u,(a[u]==a[v]?a[u]:a[u]+a[v])});
}
dfs(1,0);
for(int j = 1;j<=LOGN;j++)
{
for(int u = 1;u<=n;u++)
{
f[u][j] = f[f[u][j-1]][j-1];
val[u][j] = max(val[u][j-1],val[f[u][j-1]][j-1]);
}
}
for(int i = 1;i<=m;i++)
{
int u,v;
char c;
cin>>u>>v>>c;
int t = query(u,v);
if(u==v)
t = a[u];
//cout<<" t = "<<t<<endl;
if(c=='H')
{
if(t==1||t==3)
cout<<"1";
else cout<<"0";
}
else
{
if(t==2||t==3)
cout<<"1";
else cout<<"0";
}
}
cout<<endl;
return 0;
}
做法2:并查集
因为题目只有两种牛,我们把颜色一样的放在一个连通块里面,查询的时候看他们的父亲一不一样,如果不一样,说明有两种。如果父亲一样,那看看是不是我们要的牛。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5010,M = 2e6+10;
int n,m,a[N];
struct DSU
{
vector<int>fa,s;
int cnt;//连通块数量.
DSU(int n=1):fa(n+1,-1),s(n+1,1),cnt(n){}
int size(int x){return s[find(x)];}
int find(int x){return fa[x]==-1?x:fa[x]=find(fa[x]);}
bool connect(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return true;
else return false;
}
void merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return ;
s[x]+=s[y];
fa[y]=x;
}
};
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
DSU dsu(n);
string s;
cin>>s;
s = "?"+s;
for(int i = 1;i<n;i++)
{
int x,y;
cin>>x>>y;
if(s[x]==s[y])dsu.merge(x,y);
}
for(int i = 1;i<=m;i++)
{
int x,y;
char z;
cin>>x>>y>>z;
cout<<(!dsu.connect(x,y)||s[x]==z);
}
cout<<endl;
return 0;
}
例题2:P3398 仓鼠找 sugar
结论:判断一个结点\(x\)是否在\(s-t\)路径上
- \(dep[x]>=dep[LCA(s,t)]\)
- \(LCA(s,x)=x\)或\(LCA(t,x)=x\)
比如问:\(a->b,c->d\)路径上是否有交
\(s = lca(a,b)\) \(t = lca(c,d)\)
\(if(dep[s]<dep[t])swap(s,t),swap(a,c),swap(b,d)\)
\(dep[s]\ge dep[t]\)
即\(dep[s]\ge dep[lcd(c,d)]\)
那么如果\(lca(s,c)==s||lca(s,d)==s\)那就是有交
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int LOGN = 20;
int n, m, root, dep[N], fa[N][LOGN + 2];
vector<int> e[N];
void dfs(int u, int from)
{
dep[u] += dep[from] + 1;
for(auto v : e[u]) {
if(v == from) continue;
fa[v][0] = u;
dfs(v, u);
}
}
void lca_init()
{
for(int j = 1; j <= LOGN; j++)
for(int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
int lca_query(int u, int v)
{
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int j = LOGN; j >= 0; j--)
if(d & (1 << j))
u = fa[u][j];
if(u == v) return u;
for(int j = LOGN; j >= 0; j--)
if(fa[u][j] != fa[v][j])
u = fa[u][j],v = fa[v][j];
return fa[u][0];
}
int dist(int a,int b) {return dep[a]+dep[b]-2*dep[lca_query(a,b)];}
int main()
{
cin>>n>>m;
for(int i = 2; i <= n; i++)
{
int u, v; cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0);
lca_init();
while(m--)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
/*
判断一个结点x是否在s-t路径上
- deep[x]>=deep[LCA(s,t)]
- LCA(s,x)=x或LCA(t,x)=x;
*/
int s = lca_query(a,b);
int t = lca_query(c,d);
if(dep[s]<dep[t])
{
swap(s,t);
swap(a,c);
swap(b,d);
}
//dep[s]>=dep[t]
//dep[s]>=dep[lca(c,d)]
if(lca_query(s,c)==s||lca_query(s,d)==s)
cout<<"Y\n";
else cout<<"N\n";
}
return 0;
}
法二:判断两条路径是否有交
两个起点的距离 + 两个终点的距离 <= 两条路径的长度和
任意两点间距离\(a->b:dist[a]+dist[b]-2*dist[lca(a,b)]\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int LOGN = 20;
int n, m, root, dep[N], fa[N][LOGN + 2];
vector<int> e[N];
void dfs(int u, int from)
{
dep[u] += dep[from] + 1;
for(auto v : e[u]) {
if(v == from) continue;
fa[v][0] = u;
dfs(v, u);
}
}
void lca_init()
{
for(int j = 1; j <= LOGN; j++)
for(int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
int lca_query(int u, int v)
{
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int j = LOGN; j >= 0; j--)
if(d & (1 << j))
u = fa[u][j];
if(u == v) return u;
for(int j = LOGN; j >= 0; j--)
if(fa[u][j] != fa[v][j])
u = fa[u][j],v = fa[v][j];
return fa[u][0];
}
int dist(int a,int b) {return dep[a]+dep[b]-2*dep[lca_query(a,b)];}
int main()
{
cin>>n>>m;
for(int i = 2; i <= n; i++)
{
int u, v; cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0);
lca_init();
while(m--)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
/*
判断两条路径是否有交
两个起点的距离 + 两个终点的距离 <= 两条路径的长度和
任意两点间距离a-b:dist[a]+dist[b]-2*dist[lca(a,b)]
*/
if(dist(a,b)+dist(c,d)>=dist(a,c)+dist(b,d))
cout<<"Y\n";
else cout<<"N\n";
}
return 0;
}