树
一、树的重心
概念和性质
(1).概念
树的重心也叫树的质心。对于一棵树
(2).性质
1.树中所有点到某个点的距离和中,到重心的距离和是最小的(实际应用中经常用到此性质)。
2.把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。
3.一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。
4.一棵树最多有两个重心,且相邻。
关于如何求树的重心?
求树的重心运用动态规划的思想,也就是树上跑DP。
先任选一个结点作为根节点,把无根树变成有根树,然后去
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的距离多出了:
对于u的子树部分,那么相反的,会少了:
考虑清楚这个,接下来我们只需要先预处理出
由于
#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座核心城市的距离的最小值。那么所有非核心城市中,与核心城市的距离最大的城市,其与核心城市的距离最小。你需要求出这个最小值。
思路:我们知道,直径是树里面最长的路径。我们要找到
#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次操作,每次把
对于
对于点差分呢?和我们的边差分有所不同。
点差分裸题:有n次修改操作,每次把u..v的所有点权都加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
结论:判断一个结点
或
比如问:
即
那么如果
#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; }
法二:判断两条路径是否有交
两个起点的距离 + 两个终点的距离 <= 两条路径的长度和
任意两点间距离
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现